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
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:6Der 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:
# 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:
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:
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:
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