Eine der wichtigsten Eigenschaften des Rust-match-Konstrukts ist seine Exhaustiveness-Garantie: der Compiler verlangt, dass dein match jede mögliche Variante des gematchten Typs behandelt. Vergisst du eine, ist das Compile-Fehler, kein Runtime-Bug. Wenn das Enum später um eine Variante erweitert wird, weist der Compiler dich überall darauf hin. Diese eine Garantie eliminiert eine komplette Bug-Klasse aus deinem Code. Dieser Artikel zeigt, wie die Prüfung funktioniert, was der _-Wildcard-Arm bewirkt, wie das #[non_exhaustive]-Attribut in Public-APIs funktioniert und welche typischen Stolperfallen man kennen sollte.
Hinweis zu
&'static str: Manche Beispiele geben einen&'static strzurück — also eine Referenz auf einen Text, der für die gesamte Programm-Laufzeit gültig ist (typisch ein String-Literal aus dem Code). Die'static-Notation ist eine Lifetime, die im Lifetimes-Kapitel ausführlich erklärt wird; hier reicht das mentale Bild „Verweis auf einen festen Text".
Die Exhaustiveness-Garantie
enum Status {
Aktiv,
Inaktiv,
Geloescht,
}
fn beschreibe(s: &Status) -> &'static str {
match s {
Status::Aktiv => "Status: aktiv",
Status::Inaktiv => "Status: inaktiv",
Status::Geloescht => "Status: gelöscht",
}
}Wenn du eine Variante vergisst, gibt es einen klaren Compile-Fehler:
# enum Status { Aktiv, Inaktiv, Geloescht }
fn beschreibe(s: &Status) -> &'static str {
match s {
Status::Aktiv => "aktiv",
Status::Inaktiv => "inaktiv",
// Fehler: pattern `&Status::Geloescht` not covered
}
}Diagnose:
error[E0004]: non-exhaustive patterns: `&Status::Geloescht` not covered
--> src/main.rs:4:11
|
4 | match s {
| ^ pattern `&Status::Geloescht` not covered
|
note: `Status` defined here
--> src/main.rs:1:6Die Diagnose ist nicht nur eine generische „bitte alle Varianten behandeln"-Warnung — sie nennt genau die fehlenden Patterns, mit Verweis auf die Definition des Enums. Bei drei Varianten mag das trivial wirken, aber bei einem Enum mit zwölf Varianten und einer komplexen Funktion mit mehreren match-Statements ist diese präzise Diagnose ein echter Lebensretter.
Die wirkliche Kraft zeigt sich, wenn das Enum später erweitert wird. Fügst du eine neue Variante Status::Pending hinzu, weist dich der Compiler in jedem match in der ganzen Codebase darauf hin, dass diese neue Variante nicht behandelt wird. Eine ganze Klasse von Bugs („vergessen, die neue Variante zu handhaben") ist ausgeschlossen. In großen Codebasen mit vielen Enums ist das ein zentraler Stabilitäts-Faktor.
Der Wildcard-Arm _
Wenn du nicht alle Varianten explizit behandeln willst:
# enum Status { Aktiv, Inaktiv, Geloescht }
fn ist_aktiv(s: &Status) -> bool {
match s {
Status::Aktiv => true,
_ => false, // alle anderen Varianten
}
}Der _-Arm fungiert als Catch-all: er matcht alles, was nicht von vorherigen Armen abgedeckt wurde. Damit wird das match automatisch exhaustive — der Compiler ist zufrieden, egal welche Varianten du übersprungen hast.
Das ist syntaktisch praktisch (eine Zeile statt zehn) und semantisch oft korrekt — etwa wenn dich nur eine spezifische Variante interessiert und alle anderen gleich behandelt werden sollen. Bei ist_aktiv ist das offensichtlich: aktiv = true, alles andere = false.
Vorsicht: _ versteckt neue Varianten
Wenn du später eine neue Variante zum Enum hinzufügst:
enum Status {
Aktiv,
Inaktiv,
Geloescht,
Gesperrt, // NEU
}
fn ist_aktiv(s: &Status) -> bool {
match s {
Status::Aktiv => true,
_ => false, // fängt Gesperrt — aber soll es das?
}
}Hier liegt die Schattenseite des Wildcards: er versteckt vor dem Compiler, dass neue Varianten existieren könnten. Wenn jemand Status::Gesperrt hinzufügt, weist dich der Compiler nicht darauf hin, dass ist_aktiv jetzt auch diesen Fall behandeln muss — der _-Arm fängt ihn stillschweigend und behandelt ihn als „nicht aktiv". Ob das semantisch richtig ist, weiß nur der ursprüngliche Entwickler.
Die Empfehlung lautet daher: explizit auflisten, wo möglich. Der Compiler ist dein Freund — lass ihn dir bei jeder neuen Variante mitteilen, dass du nochmal über die Semantik nachdenken musst. Nur bei sehr breiten Enums (etwa io::ErrorKind mit 40+ Varianten) ist _ pragmatisch. Auch dann ist ein Kommentar wie _ => /* siehe handler::all_other_errors */ hilfreich, um die Absicht zu dokumentieren.
Strukturierte Wildcard mit Binding
Wenn du wissen willst, was unter dem _ lief:
enum Cmd {
Start,
Stop,
Pause,
Custom(String),
}
fn handle(c: &Cmd) {
match c {
Cmd::Start => println!("Start"),
Cmd::Stop => println!("Stop"),
anderer => println!("Andere: {:?}", anderer_name(anderer)),
}
}
# fn anderer_name(c: &Cmd) -> &'static str { "x" }Statt eines anonymen _ kannst du einen Namen im Catch-all-Arm verwenden — etwa anderer. Das bindet die nicht-spezifisch-gematchte Variante an diesen Namen, und du kannst sie im Arm-Body weiter verarbeiten. Sehr nützlich für Logging der unerwarteten Werte: statt nur „andere Variante" zu loggen, kannst du den tatsächlichen Wert mit {anderer:?} ausgeben.
Das ist auch eine schöne Defensiv-Programmierungs-Variante: deine Hauptlogik behandelt die erwarteten Varianten explizit, der Catch-all loggt alles Unerwartete für späteres Debugging. Bei Production-Code, der mit dynamischen Daten umgeht (Parser, Protokoll-Handler, externe APIs), ist dieser Pattern Gold wert.
Exhaustiveness auf Daten
Die Prüfung gilt nicht nur für Varianten, sondern auch für Daten innerhalb der Variante:
fn klassifiziere(n: i32) -> &'static str {
match n {
0 => "null",
1..=9 => "klein",
10..=99 => "mittel",
// Compiler-Fehler: nicht alle i32-Werte abgedeckt
// (z. B. negative, große)
}
}Bei numerischen Typen ist _ fast immer nötig — du kannst nicht alle 4 Milliarden i32-Werte auflisten:
fn klassifiziere(n: i32) -> &'static str {
match n {
0 => "null",
1..=9 => "klein",
10..=99 => "mittel",
_ => "anderes", // alle übrigen
}
}#[non_exhaustive] — vorwärtskompatible Enums
Library-Crates wollen oft die Möglichkeit offen halten, in Zukunft neue Varianten hinzuzufügen, ohne dass Konsumenten brechen:
#[non_exhaustive]
pub enum HttpError {
BadRequest,
Unauthorized,
NotFound,
ServerError,
}Im Library-Crate: ganz normal nutzbar.
Im Konsumenten-Crate:
# mod externe_lib {
# #[non_exhaustive]
# pub enum HttpError { BadRequest, Unauthorized, NotFound, ServerError }
# }
use externe_lib::HttpError;
fn beschreibe(e: HttpError) -> &'static str {
match e {
HttpError::BadRequest => "400",
HttpError::Unauthorized => "401",
HttpError::NotFound => "404",
HttpError::ServerError => "500",
_ => "anderer Fehler", // PFLICHT — Compiler verlangt es
}
}Selbst wenn du aktuell alle bekannten Varianten abdeckst, verlangt der Compiler beim Konsumenten einen _-Arm. Damit kann die Library neue Varianten hinzufügen, ohne den Konsumenten zu brechen.
Wann nutzen: in jedem Public-API-Enum, das in Zukunft erweitert werden könnte. Standard-Pattern in stabilen Stdlib- und Crates-Bibliotheken.
Guards und Exhaustiveness
Match-Arme können Guards mit if-Bedingungen haben:
fn beschreibe(n: i32) -> &'static str {
match n {
x if x < 0 => "negativ",
0 => "null",
x if x > 100 => "groß",
_ => "klein bis mittel",
}
}Wichtig: Guards werden vom Exhaustiveness-Checker als nicht alle Fälle abdeckend behandelt. Selbst wenn deine Guards logisch alle Werte abdecken, verlangt der Compiler trotzdem einen Catch-All-Arm.
fn beschreibe(n: i32) -> &'static str {
match n {
x if x < 0 => "negativ",
x if x == 0 => "null",
x if x > 0 => "positiv",
// Compiler beschwert sich — Guards sind nicht „bewiesen exhaustive"
// _ => unreachable!(), // Lösung
}
}Der Compiler kann die Guard-Bedingungen nicht analysieren. Lösung: ein finaler _ => unreachable!()-Arm.
Or-Patterns
Mehrere Patterns mit | kombiniert:
fn ist_inaktiv(s: &Status) -> bool {
match s {
Status::Inaktiv | Status::Geloescht => true,
Status::Aktiv => false,
}
}
# enum Status { Aktiv, Inaktiv, Geloescht }Status::Inaktiv | Status::Geloescht matcht beide Varianten mit derselben Aktion. Reduziert Boilerplate erheblich.
Praxis: Exhaustive Match im echten Code
Domain-Status
pub enum OrderStatus {
Erstellt,
Bezahlt,
Versendet,
Geliefert,
Storniert,
}
pub fn naechster_status(aktuell: OrderStatus) -> Option<OrderStatus> {
match aktuell {
OrderStatus::Erstellt => Some(OrderStatus::Bezahlt),
OrderStatus::Bezahlt => Some(OrderStatus::Versendet),
OrderStatus::Versendet => Some(OrderStatus::Geliefert),
OrderStatus::Geliefert => None, // Endzustand
OrderStatus::Storniert => None,
}
}Alle 5 Varianten explizit. Wenn später Zurueckgesandt dazukommt, weist der Compiler darauf hin.
HTTP-Status-Code-Klassifizierung
pub fn klasse(code: u16) -> &'static str {
match code {
100..=199 => "1xx Informational",
200..=299 => "2xx Success",
300..=399 => "3xx Redirect",
400..=499 => "4xx Client Error",
500..=599 => "5xx Server Error",
_ => "Unknown",
}
}Range-Patterns plus _ für alle anderen. Numerischer Match braucht fast immer Wildcard.
Event-Handling mit Or-Pattern
pub enum Event {
Click(u32, u32),
DoubleClick(u32, u32),
KeyDown(char),
KeyUp(char),
MouseMove(u32, u32),
Resize(u32, u32),
}
pub fn ist_pointer_event(e: &Event) -> bool {
match e {
Event::Click(_, _) | Event::DoubleClick(_, _) | Event::MouseMove(_, _) => true,
Event::KeyDown(_) | Event::KeyUp(_) => false,
Event::Resize(_, _) => false,
}
}Or-Patterns gruppieren ähnliche Events. Wenn neue Event-Typen kommen, sieht der Compiler die fehlende Behandlung.
Mehr-Varianten-Enum mit non_exhaustive
#[non_exhaustive]
pub enum ApiError {
BadRequest,
Unauthorized,
Forbidden,
NotFound,
Conflict,
ServerError,
ServiceUnavailable,
}
pub fn http_status(e: &ApiError) -> u16 {
match e {
ApiError::BadRequest => 400,
ApiError::Unauthorized => 401,
ApiError::Forbidden => 403,
ApiError::NotFound => 404,
ApiError::Conflict => 409,
ApiError::ServerError => 500,
ApiError::ServiceUnavailable => 503,
_ => 500, // Pflicht wegen non_exhaustive
}
}Library-Pattern: non_exhaustive plus _-Arm im Konsumenten — zukunftssicher.
State-Machine-Übergänge
pub enum ConnState {
Inaktiv,
Verbindet,
Authentifiziert,
Aktiv,
Geschlossen,
}
pub fn darf_senden(s: &ConnState) -> bool {
match s {
ConnState::Aktiv => true,
ConnState::Inaktiv | ConnState::Verbindet
| ConnState::Authentifiziert | ConnState::Geschlossen => false,
}
}Or-Pattern alle Nicht-Aktiv-States. Wenn neuer State dazukommt, muss explizit entschieden werden, ob er senden darf.
Bei Daten: explizite Behandlung
pub enum LogLevel {
Debug,
Info,
Warn,
Error,
Custom(u32),
}
pub fn level_zahl(l: LogLevel) -> u32 {
match l {
LogLevel::Debug => 0,
LogLevel::Info => 1,
LogLevel::Warn => 2,
LogLevel::Error => 3,
LogLevel::Custom(n) => n,
}
}Custom(n) bindet den inneren Wert. Wenn Custom-Varianten weggelassen werden, gibt's einen Compile-Fehler.
Nested Match
pub enum DbResult {
Erfolg(Vec<String>),
ZeitUeberschritten,
Fehler { code: i32, nachricht: String },
}
pub fn beschreibe(r: &DbResult) -> String {
match r {
DbResult::Erfolg(reihen) if reihen.is_empty() => "leer".to_string(),
DbResult::Erfolg(reihen) => format!("{} Reihen", reihen.len()),
DbResult::ZeitUeberschritten => "Timeout".to_string(),
DbResult::Fehler { code, nachricht } => format!("[{code}] {nachricht}"),
}
}Guards plus normale Patterns gemischt. Der Compiler erkennt, dass die beiden Erfolg-Arme zusammen alle Erfolg-Fälle abdecken (leer + nicht-leer).
Mit unreachable! als Sicherheit
fn signum(n: i32) -> i32 {
match n {
x if x > 0 => 1,
x if x < 0 => -1,
0 => 0,
_ => unreachable!("alle Fälle abgedeckt"),
}
}unreachable!() panickt zur Laufzeit, falls je erreicht — was Logik-Bugs sofort sichtbar macht. Trotzdem erfüllt es exhaustive-Anforderung.
Häufige Stolperfallen
_ versteckt neue Varianten.
Wenn du _ => ... schreibst, fängt er auch zukünftig hinzugefügte Varianten — möglicherweise mit falscher Semantik. Bei deinen eigenen Enums (vollständig sichtbar): explizit auflisten. Compiler-Warnings sind dein Freund.
Bei #[non_exhaustive]-Enums ist _ Pflicht.
Library-Konsumenten müssen einen _-Arm haben — sonst Compile-Fehler bei Updates. Das ist die Verträge des Library-Autors: „ich behalte mir vor, Varianten hinzuzufügen, du musst sie tolerieren".
Guards machen Exhaustiveness nicht entscheidbar.
match n { x if x > 0 => ..., x if x < 0 => ..., 0 => ... } ist logisch komplett, aber der Compiler kann das nicht verifizieren. Lösung: _ => unreachable!() als letzter Arm.
Range-Patterns bei Integern brauchen meist _.
match n: i32 { 0..=99 => ..., 100..=999 => ... } ist nicht exhaustive — negative Werte und 1000+ fehlen. Wildcard nötig.
Match auf Or-Patterns ist sehr expressiv.
A | B | C => ... für gemeinsame Behandlung mehrerer Varianten. Beim Refactoring (eine Variante umbenennen): Or-Patterns geben dem Compiler die Chance, alle Stellen zu finden.
todo!() und unimplemented!() als Platzhalter.
Während der Entwicklung: ein Arm => todo!() macht das match exhaustive, aber panickt zur Laufzeit. Du kannst weiterprogrammieren und vergisst die Stelle nicht.
Reihenfolge der Arme ist relevant.
Der Compiler probiert die Arme von oben nach unten — der erste passende gewinnt. Spezifische Patterns kommen vor allgemeineren. Some(0) => ... vor Some(_) => .... Falsche Reihenfolge führt zu Dead-Code-Warnings.
Match auf Referenzen automatisch.
match &enum_value matcht auf Patterns wie Variant(x) — der & wird automatisch via „match ergonomics" eingefügt. x wird zur Referenz. Sehr bequem, manchmal aber irritierend (Wert vs. Referenz im Pattern).
Weiterführende Ressourcen
Externe Quellen
- The Rust Book – The match Control Flow Construct
- Rust Reference – Match expressions
- Rust Reference – non_exhaustive attribute
- rustc Error E0004
- Rust by Example – Match