Pattern-Matching ist eines der ausdruckstärksten Sprach-Features in Rust. Patterns leben nicht nur in match-Armen, sondern auch in let-Bindungen, if let, while let, for-Loops, Funktions-Parametern und let else. Dieser Artikel ist eine systematische Referenz: jede Pattern-Form mit Syntax, Anwendung und Beispiel. Von simplen Literalen über Struct-Destrukturierung bis zu @-Bindings und Guard-Bedingungen. Wer alle Pattern-Formen kennt, schreibt Code, der bestimmte Aufgaben in einer Zeile löst, für die andere Sprachen mehrere Code-Blöcke brauchen.
Literal-Patterns
Konkrete Werte als Pattern:
fn beschreibe(n: i32) -> &'static str {
match n {
0 => "null",
1 => "eins",
42 => "die Antwort",
_ => "anderes",
}
}
fn main() {
assert_eq!(beschreibe(42), "die Antwort");
}Literal-Patterns funktionieren auf allen Typen mit PartialEq: i32, &str, bool, char und mehr.
fn klassifiziere_typ(s: &str) -> &'static str {
match s {
"" => "leer",
"ja" | "yes" | "y" => "positiv",
"nein" | "no" | "n" => "negativ",
_ => "anderes",
}
}Range-Patterns
Wertebereiche mit ..= (inklusiv) oder .. (exklusiv, nur in Slice-Patterns):
fn klassifiziere_alter(jahre: u32) -> &'static str {
match jahre {
0..=12 => "Kind",
13..=17 => "Teenager",
18..=64 => "Erwachsen",
65..=u32::MAX => "Senior",
}
}
fn main() {
assert_eq!(klassifiziere_alter(8), "Kind");
assert_eq!(klassifiziere_alter(35), "Erwachsen");
}Range-Patterns funktionieren auf Integer-Typen und auf char:
fn klassifiziere_zeichen(c: char) -> &'static str {
match c {
'0'..='9' => "Ziffer",
'a'..='z' => "Kleinbuchstabe",
'A'..='Z' => "Großbuchstabe",
_ => "Anderes",
}
}Struct-Patterns
Destrukturierung von Structs mit benannten Feldern:
struct Punkt { x: f64, y: f64 }
fn beschreibe(p: Punkt) -> String {
match p {
Punkt { x: 0.0, y: 0.0 } => "Ursprung".to_string(),
Punkt { x, y: 0.0 } => format!("Auf x-Achse bei {x}"),
Punkt { x: 0.0, y } => format!("Auf y-Achse bei {y}"),
Punkt { x, y } => format!("({x}, {y})"),
}
}Selektive Bindings mit ..
struct Person { name: String, alter: u32, email: String }
fn beschreibe(p: &Person) -> String {
match p {
Person { name, .. } => format!("Name: {name}"),
}
}.. ignoriert die restlichen Felder. Sehr nützlich, wenn dich nur eines interessiert.
Umbenennen
# struct Person { name: String, alter: u32, email: String }
fn extrahieren(p: Person) {
let Person { name: vorname, alter: jahre, .. } = p;
println!("{vorname} ({jahre} Jahre)");
}name: vorname bindet das name-Feld an die lokale Variable vorname. Praktisch bei Konflikten oder klareren lokalen Namen.
Tuple-Patterns
Destrukturierung von Tupeln und Tuple-Structs:
fn analysiere(paar: (i32, i32)) -> &'static str {
match paar {
(0, 0) => "Ursprung",
(x, 0) if x > 0 => "rechts der Achse",
(x, 0) if x < 0 => "links der Achse",
(0, _) => "auf y-Achse",
_ => "anderswo",
}
}struct Coord(f64, f64);
fn beschreibe(c: Coord) -> String {
match c {
Coord(x, y) if x == y => format!("Diagonale bei {x}"),
Coord(x, y) => format!("({x}, {y})"),
}
}Slice-Patterns
Pattern-Matching auf Slices — vollständig im Slices-Kapitel behandelt, hier eine Zusammenfassung:
fn beschreibe(s: &[i32]) -> &'static str {
match s {
[] => "leer",
[_] => "ein Element",
[_, _] => "zwei Elemente",
[first, .., last] if first == last => "erstes = letztes",
[first, .., last] => "Mehrelementig",
_ => "irgendwas",
}
}[a, .., b], [a, rest @ ..], [a, b, c] — siehe Slice-Patterns-Artikel im Slices-Kapitel.
@-Bindings (Binding mit Test)
Manchmal willst du gleichzeitig matchen und einen Namen binden:
fn klassifiziere(n: i32) -> String {
match n {
klein @ 0..=9 => format!("klein: {klein}"),
mittel @ 10..=99 => format!("mittel: {mittel}"),
gross => format!("groß: {gross}"),
}
}
fn main() {
assert_eq!(klassifiziere(5), "klein: 5");
assert_eq!(klassifiziere(50), "mittel: 50");
assert_eq!(klassifiziere(500), "groß: 500");
}klein @ 0..=9 matcht 0..=9 und bindet den Wert an klein. Ohne @ müsstest du match n { x if (0..=9).contains(&x) => format!(...) } schreiben — viel verbose.
@ mit Struct-Patterns
struct User { id: u64, name: String }
fn beschreibe(u: User) -> String {
match u {
User { id: admin_id @ 1..=10, name } =>
format!("Admin {admin_id}: {name}"),
u @ User { .. } => format!("Normal: {}", u.name),
}
}Im ersten Arm: id wird auf 1..=10 getestet und an admin_id gebunden. Im zweiten: der gesamte User wird an u gebunden.
ref und ref mut — Bindings als Referenz
Standard-Bindings nehmen den Wert per Move (oder Copy bei Copy-Typen). Mit ref bekommst du eine Referenz:
let owned = String::from("Hi");
match owned {
ref s => println!("{s}"), // s: &String, owned bleibt nutzbar
}
println!("{owned}"); // ok — nicht gemovedref s matcht den Wert und bindet s als shared reference, ohne den Original-Wert zu verbrauchen.
ref mut
let mut v = vec![1, 2, 3];
match v {
ref mut vec_ref => vec_ref.push(99),
}
assert_eq!(v, vec![1, 2, 3, 99]);ref mut bindet als mutable Referenz.
In modernem Code: meist nicht nötig
Mit „match ergonomics" (seit Rust 2018) wird ref selten gebraucht. Wenn du match &val { ... } schreibst, werden Bindings automatisch zu Referenzen:
let owned = String::from("Hi");
match &owned {
s => println!("{s}"), // s ist automatisch &String
}
println!("{owned}"); // okref wird heute nur noch in seltenen Sonderfällen explizit gebraucht (z. B. beim Destrukturieren von owned Werten in einem Pattern, wo du selektiv borrowen willst).
Guards mit if
Eine Pattern kann mit einem Guard ergänzt werden — einer if-Bedingung, die zusätzlich erfüllt sein muss:
fn beschreibe(n: i32) -> &'static str {
match n {
x if x < 0 => "negativ",
0 => "null",
x if x % 2 == 0 => "positiv und gerade",
x if x % 2 != 0 => "positiv und ungerade",
_ => unreachable!(),
}
}Guards sind beliebige Bool-Expressions. Sie haben Zugriff auf alle im Pattern gebundenen Variablen.
Guards mit Struct-Destrukturierung
struct Order { kunde: String, betrag_cent: i64 }
fn rabatt(o: &Order) -> u32 {
match o {
Order { betrag_cent, .. } if *betrag_cent > 10_000 => 15,
Order { kunde, .. } if kunde.starts_with("VIP_") => 10,
_ => 0,
}
}Destrukturierung plus Guard für „matche und prüfe noch eine zusätzliche Bedingung".
Or-Patterns mit |
Mehrere Patterns mit derselben Aktion:
fn ist_vokal(c: char) -> bool {
match c {
'a' | 'e' | 'i' | 'o' | 'u' => true,
'A' | 'E' | 'I' | 'O' | 'U' => true,
_ => false,
}
}Or-Patterns funktionieren mit Literalen, Ranges, Enum-Varianten, Struct-Patterns — überall.
Refutable vs. Infallible Patterns
Es gibt zwei Pattern-Kategorien:
- Refutable — kann fehlschlagen. Beispiel:
Some(x)matchtNonenicht. - Infallible — matcht garantiert. Beispiel:
(a, b)matcht jedes(i32, i32).
Wo welche erlaubt sind:
| Kontext | Refutable erlaubt? |
|---|---|
let pattern = value; | nur infallible |
match value { pattern => ... } | ja |
if let pattern = value { ... } | ja |
while let pattern = value { ... } | ja |
let pattern = value else { return; }; | ja (refutable) |
for pattern in iter { ... } | nur infallible |
| Funktions-Parameter | nur infallible |
fn main() {
// let mit refutable: Compile-Fehler
// let Some(n) = Some(42);
// match: refutable ok
match Some(42) {
Some(n) => println!("{n}"),
None => println!("nichts"),
}
// if let: refutable ok
if let Some(n) = Some(42) {
println!("{n}");
}
// let mit infallible: ok
let (a, b) = (1, 2);
// let else: refutable ok, else muss divergent sein
// let Some(n) = Some(42) else { return; };
}Praxis: Patterns im echten Code
Token-Verarbeitung mit Or-Patterns
enum Token { Plus, Minus, Mal, Geteilt, Zahl(i32), Klammer(char), Ende }
fn ist_operator(t: &Token) -> bool {
matches!(t, Token::Plus | Token::Minus | Token::Mal | Token::Geteilt)
}
fn prioritaet(t: &Token) -> Option<u32> {
match t {
Token::Plus | Token::Minus => Some(1),
Token::Mal | Token::Geteilt => Some(2),
_ => None,
}
}matches! ist syntaktischer Zucker für „match auf bool". Or-Patterns gruppieren ähnliche Varianten.
Range-basierte Klassifikation
pub fn klassifiziere(code: u16) -> &'static str {
match code {
100..=199 => "Informational",
200..=299 => "Success",
300..=399 => "Redirect",
400..=499 => "Client Error",
500..=599 => "Server Error",
_ => "Unknown",
}
}Struct-Destrukturierung in Funktions-Parametern
struct Punkt { x: f64, y: f64 }
fn distanz(Punkt { x: x1, y: y1 }: Punkt, Punkt { x: x2, y: y2 }: Punkt) -> f64 {
((x2 - x1).powi(2) + (y2 - y1).powi(2)).sqrt()
}
fn main() {
let d = distanz(
Punkt { x: 0.0, y: 0.0 },
Punkt { x: 3.0, y: 4.0 },
);
assert_eq!(d, 5.0);
}Destrukturierung direkt im Parameter — kein zusätzlicher let-Schritt.
Guard für komplexe Bedingungen
enum Anfrage {
Lesen { pfad: String, user: String },
Schreiben { pfad: String, user: String, daten: Vec<u8> },
}
pub fn ist_authorisiert(a: &Anfrage, admin_pfade: &[&str]) -> bool {
match a {
Anfrage::Lesen { pfad, user } if user == "admin" => true,
Anfrage::Lesen { pfad, .. } if !admin_pfade.contains(&pfad.as_str()) => true,
Anfrage::Schreiben { user, .. } if user == "admin" => true,
_ => false,
}
}Authentifizierungs-Logik mit Patterns plus Guards.
@-Binding für komplexe Validierung
pub fn klassifiziere_port(port: u16) -> String {
match port {
priv_port @ 0..=1023 => format!("privilegiert: {priv_port}"),
user_port @ 1024..=49151 => format!("user: {user_port}"),
dyn_port @ 49152.. => format!("dynamisch: {dyn_port}"),
}
}@-Binding macht den Wert mit Range-Test gleichzeitig verfügbar.
Nested Pattern mit Struct + Enum
struct Event { typ: EventTyp, timestamp: u64 }
enum EventTyp { Login(String), Logout, Error(u32) }
pub fn handle(e: &Event) {
match e {
Event { typ: EventTyp::Login(user), timestamp } =>
println!("[{timestamp}] Login: {user}"),
Event { typ: EventTyp::Error(code), .. } if *code >= 500 =>
println!("Server-Error: {code}"),
Event { typ: EventTyp::Error(code), .. } =>
println!("Client-Error: {code}"),
Event { typ: EventTyp::Logout, .. } =>
println!("Logout"),
}
}Verschachtelte Patterns sind sehr expressiv — Struct mit innerem Enum, alle Felder destrukturierbar.
Or-Pattern mit Bindings
enum Nachricht {
Hallo(String),
Tschuess(String),
Custom { typ: String, text: String },
}
pub fn extrahiere_text(n: &Nachricht) -> &str {
match n {
Nachricht::Hallo(t) | Nachricht::Tschuess(t) => t,
Nachricht::Custom { text, .. } => text,
}
}Or-Pattern mit gemeinsamem Bindung — t ist in beiden Armen ein &String.
Match auf Tuple für State-Übergänge
#[derive(Debug, PartialEq)]
enum State { Init, Connecting, Connected, Closed }
#[derive(Debug)]
enum Event { Start, Success, Timeout, Close }
pub fn naechster_state(s: &State, e: &Event) -> State {
match (s, e) {
(State::Init, Event::Start) => State::Connecting,
(State::Connecting, Event::Success) => State::Connected,
(State::Connecting, Event::Timeout) => State::Init,
(State::Connected, Event::Close) => State::Closed,
(s, _) => match s {
State::Init => State::Init,
State::Connecting => State::Connecting,
State::Connected => State::Connected,
State::Closed => State::Closed,
},
}
}State-Machines mit match (state, event) — der idiomatische Stil in Rust.
Verschachtelte Option-Matches
pub fn finde_email(user: Option<&str>, fallback: Option<&str>) -> &'static str {
match (user, fallback) {
(Some(u), _) if u.contains('@') => "primary",
(_, Some(f)) if f.contains('@') => "fallback",
_ => "kein gültiger Wert",
}
}Tuple-Pattern auf zwei Optionen mit Guards.
Besonderheiten
Patterns funktionieren überall, wo Werte gebunden werden.
let, match, if let, while let, let else, for, Funktions-Parameter — alle akzeptieren Patterns. Sehr viele Code-Stellen können dadurch elegant destrukturieren.
Refutable Patterns sind in let verboten.
let Some(n) = x; ist Compile-Fehler, weil Some(n) None nicht matcht. Lösung: let else mit divergentem Branch, oder match/if let.
Or-Patterns dürfen Bindings haben — aber Typ muss gleich sein.
Some(x) | Other(x) funktioniert, wenn x in beiden den gleichen Typ hat. Sonst Compile-Fehler. Sehr nützlich zum Gruppieren von Enum-Varianten mit gleicher Daten-Form.
@-Binding für „matche und behalte".
klein @ 0..=9 matcht das Range und gibt dir gleichzeitig den Wert. Ohne @ müsstest du den Wert separat extrahieren oder einen Guard nutzen. Eleganter Shortcut.
.. ignoriert „den Rest".
In Struct-Patterns: Person { name, .. } — alle anderen Felder ignoriert. In Tuple-Patterns: (first, ..). In Slice-Patterns: [first, ..]. Sehr verbreitet, wenn nur ein Teil interessiert.
ref wird meist nicht mehr gebraucht.
Mit „match ergonomics" matcht match &val { Some(x) => ... } automatisch x als &T. Früher brauchtest du Some(ref x). Heute selten — nur bei selektiven Borrows in Patterns auf owned Werten.
Guards laufen NACH dem Pattern-Match.
Pattern if condition => ... — erst matcht das Pattern, dann wird die Bedingung geprüft. Wenn die Bedingung false ist, geht's zum nächsten Arm. Die Guard-Bedingung hat Zugriff auf alle gebundenen Variablen aus dem Pattern.
Patterns auf Tuple sind extrem mächtig für State-Machines.
match (state, event) { ... } destrukturiert beide Komponenten gleichzeitig. Standard-Pattern für State-Übergänge — jeder Arm ein konkreter Übergang.
Weiterführende Ressourcen
Externe Quellen
- The Rust Book – Patterns and Matching
- Rust Reference – Patterns
- Rust Reference – Refutability
- Rust by Example – Pattern Matching