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.

Die Exhaustiveness-Garantie

Rust Exhaustive match
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:

Rust Fehlender Arm
# 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:

Rust rustc-Output
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:6

Der Compiler zeigt genau, welche Variante fehlt. Mit drei Varianten ist das trivial; mit zwölf Varianten ein Lebensretter.

Der Wildcard-Arm _

Wenn du nicht alle Varianten explizit behandeln willst:

Rust Mit Wildcard
# enum Status { Aktiv, Inaktiv, Geloescht }
fn ist_aktiv(s: &Status) -> bool {
    match s {
        Status::Aktiv => true,
        _ => false,         // alle anderen Varianten
    }
}

Der _-Arm fängt alles, was nicht von vorigen Armen gematcht wurde. Er macht das match automatisch exhaustive — der Compiler ist zufrieden.

Vorsicht: _ versteckt neue Varianten

Wenn du später eine neue Variante zum Enum hinzufügst:

Rust Neue Variante
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?
    }
}

Der _-Arm matcht jetzt auch Gesperrt — möglicherweise mit falscher Semantik. _ versteckt die Information, dass neue Varianten existieren.

Empfehlung: explizit auflisten, wo möglich. Der Compiler ist dein Freund. Nur bei sehr breiten Enums (z. B. io::ErrorKind mit 40+ Varianten) ist _ pragmatisch.

Strukturierte Wildcard mit Binding

Wenn du wissen willst, was unter dem _ lief:

Rust Catch-all mit Name
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 _ ein Name wie anderer — bindet die Variante. Manchmal nützlich für Logging der unerwarteten Werte.

Exhaustiveness auf Daten

Die Prüfung gilt nicht nur für Varianten, sondern auch für Daten innerhalb der Variante:

Rust Datenprüfung
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:

Rust Mit Wildcard
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:

Rust non_exhaustive
#[non_exhaustive]
pub enum HttpError {
    BadRequest,
    Unauthorized,
    NotFound,
    ServerError,
}

Im Library-Crate: ganz normal nutzbar.

Im Konsumenten-Crate:

Rust Konsument-Code
# 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:

Rust Guards
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.

Rust Guard-Falle
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:

Rust Or-Pattern
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

Rust Order-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

Rust HTTP-Klassen
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

Rust Event-Dispatch
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

Rust Library-Error
#[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

Rust Connection-State
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

Rust Mit Daten
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

Rust Verschachtelt
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

Rust unreachable
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 &gt; 0 =&gt; ..., x if x &lt; 0 =&gt; ..., 0 =&gt; ... } 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 =&gt; ..., 100..=999 =&gt; ... } 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) =&gt; ... vor Some(_) =&gt; .... 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

/ Weiter

Zurück zu Enums & Pattern Matching

Zur Übersicht