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.
Hinweis zu
&'static str: Einige Beispiele in diesem Katalog geben einen Wert vom Typ&'static strzurück — also eine Referenz auf einen Text, der für die gesamte Programm-Laufzeit gültig ist (klassisch ein String-Literal). Die'static-Notation ist eine Lifetime und wird im Lifetimes-Kapitel ausführlich erklärt; für die Patterns hier reicht das mentale Bild „ein Verweis auf einen fest im Code stehenden Text".
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");
}Die einfachste Form eines Patterns ist ein konkreter Wert. Im Match-Arm steht statt einer Variable oder Wildcard ein Literal: 0, 1, "hello", true. Der Arm matcht nur, wenn der gematchte Wert exakt diesem Literal entspricht.
Literal-Patterns funktionieren auf allen Typen, die PartialEq implementieren — i32, &str, bool, char, Floats (mit der NaN-Einschränkung), eigene Typen mit PartialEq-Derive. Bei Strings ist die Vergleichs-Operation byte-weise; bei Floats ist es das übliche IEEE-754-Verhalten.
Die Or-Pattern-Form "ja" | "yes" | "y" zeigt, wie du mehrere Literale in einem Arm kombinieren kannst — alle drei führen zum gleichen Ergebnis. Sehr typisch für „natürliche Sprache"-Parsing oder Synonyme.
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 matchen einen Wertebereich statt eines einzelnen Werts. Die Syntax 0..=12 matcht alle Werte zwischen 0 und 12 (inklusive beider Grenzen). Bei der Klassifikation von Altersgruppen oder Bereichen ist das viel kürzer als manuelle if-else-Ketten.
Wichtig: in match-Patterns wird typischerweise ..= (inklusive Ende) verwendet, weil das ..-Pattern (exklusiv) aktuell nur in Slice-Patterns erlaubt ist. Bei 0..=u32::MAX müsste eigentlich der ganze u32-Wertebereich zu Hause sein — der Compiler sieht das auch so und gibt keine „nicht-exhaustive"-Warnung.
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}"),
}
}Die ..-Notation in Struct-Patterns ist eine Convenience: sie steht für „alle übrigen Felder ignorieren". Bei einem Struct mit zehn Feldern, von denen dich nur eines interessiert, sparst du dir, neun mal _ zu schreiben. Du sagst direkt: „nimm name, der Rest ist egal".
Das ist sowohl ein syntaktischer als auch ein semantischer Vorteil: wenn später neue Felder zum Struct hinzukommen, bleibt das Pattern gültig — .. deckt sie automatisch mit ab. Bei explizit aufgelisteten Feldern müsstest du das Pattern jeweils anpassen.
Umbenennen
# struct Person { name: String, alter: u32, email: String }
fn extrahieren(p: Person) {
let Person { name: vorname, alter: jahre, .. } = p;
println!("{vorname} ({jahre} Jahre)");
}Das Umbenennen-Pattern feld: lokal bindet ein Struct-Feld an einen lokal anderen Namen. Bei Person { name: vorname, alter: jahre } heißt das: nimm das name-Feld der Person und bind es lokal an die Variable vorname; ebenso alter an jahre.
Anwendungsfälle: wenn die Struct-Feldnamen mit lokalen Variablen kollidieren würden (etwa weil du schon ein name im Scope hast), oder wenn lokal andere Namen sinnvoller sind (etwa weil der Code-Kontext anders ist). Auch bei Cross-Domain-Mappings hilfreich — du destrukturierst eine User-Struct mit den Domain-Namen in lokale Variablen mit Code-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",
}
}Slice-Patterns sind eine ganze Pattern-Familie für sich — siehe den eigenen Artikel im Slices-Kapitel. Hier nur die Kurzform: [] für leeren Slice, [_] für ein Element, [a, b, c] für genau drei Elemente, [first, .., last] mit Rest-Operator. Mit @ lassen sich Rest-Slices an Namen binden: [head, tail @ ..].
Die Kombination aus Slice-Patterns und Guards ist sehr ausdrucksstark: [first, .., last] if first == last matcht Slices mit mindestens zwei Elementen, deren erstes und letztes Element gleich sind. Solche kompakten Beschreibungen wären in vielen Sprachen umständliche Index-Berechnungen.
@-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");
}Das @-Binding (auch „at-Pattern" genannt) kombiniert zwei Pattern-Operationen: es matcht auf eine Bedingung und bindet gleichzeitig den gematchten Wert an einen Namen. Die Syntax name @ pattern matcht das Pattern und bindet den Gesamtwert (nicht ein Sub-Feld) an name.
Im Beispiel matcht klein @ 0..=9 jeden Wert im Range 0-9 und bindet ihn an klein. Damit kannst du im Arm-Body sowohl wissen „es ist im Range" als auch den konkreten Wert weiterverarbeiten.
Ohne @ müsstest du entweder den Wert in einer separaten Bindung halten (let klein = n; match klein { ... }) oder Guards verwenden (match n { x if (0..=9).contains(&x) => format!(...) }). Beides ist verbose. Das @-Binding macht die Intention direkt sichtbar.
@ 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 gemovedStandard-Bindings in Patterns nehmen den Wert per Move (oder Copy, falls der Typ Copy ist). Bei einem String würde ein normales match owned { s => ... } den String konsumieren — owned wäre danach nicht mehr nutzbar.
Mit ref s bekommst du stattdessen eine shared Reference (&String) gebunden. Der Original-Wert wird nicht verbraucht und bleibt nach dem Match nutzbar. Das ist analog zum &-Operator beim Pattern selbst — nur dass ref auf der Binding-Seite des Patterns steht, nicht auf der Wert-Seite.
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}"); // okMit den Match Ergonomics (seit Rust 2018) ist der ref-Operator in den meisten Fällen überflüssig geworden. Wenn du match &val { ... } schreibst, werden die Bindings automatisch zu Referenzen — der Compiler erkennt, dass du auf einer Referenz matchst, und passt die Binding-Modi entsprechend an.
Das ist sehr viel angenehmer zu lesen und zu schreiben als der explizite ref-Operator. In modernem Code siehst du ref nur noch in seltenen Spezialfällen: beim Destrukturieren von owned Werten in einem Pattern, wo du nur einzelne Felder borrowen willst (während andere gemoved werden), oder in alter Codebase, die Pre-2018-Stil verwendet.
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 die mächtige Erweiterung, mit der du beliebige boolesche Bedingungen ans Pattern anhängen kannst. Die Syntax pattern if condition => arm matcht nur dann, wenn sowohl das Pattern passt als auch die Bedingung wahr ist.
Guards haben Zugriff auf alle im Pattern gebundenen Variablen — im Beispiel kann der x % 2 == 0-Guard auf das vom Pattern gebundene x zugreifen. Damit erweiterst du die Match-Logik um Berechnungen, die mit reinen strukturellen Patterns nicht ausdrückbar wären.
Wichtig: der Exhaustiveness-Checker betrachtet Guards als „opaque" — er weiß nicht, dass if x % 2 == 0 und if x % 2 != 0 zusammen alle Werte abdecken. Daher der unreachable!() am Ende: er sagt dem Compiler „dieser Fall kann nicht eintreten" und beruhigt die Exhaustiveness-Prüfung. Wenn du sicher bist, ist unreachable!() korrekt; wenn nicht, ein Panic-Risiko.
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,
}
}Mit dem |-Operator (gesprochen „or") kannst du mehrere alternative Patterns in einem Arm zusammenfassen, die alle zum gleichen Body führen. 'a' | 'e' | 'i' | 'o' | 'u' matcht jedes der fünf Vokale; alle führen zum gleichen => true.
Or-Patterns funktionieren mit allen Pattern-Formen — Literalen, Ranges, Enum-Varianten, Struct-Patterns, geschachtelt. Voraussetzung ist, dass alle alternativen Patterns dieselben Bindungen (mit denselben Typen) deklarieren — sonst kann der Body nicht eindeutig die gebundenen Variablen verwenden. Bei Status::Ok(n) | Status::Warning(n) ist das erfüllt; bei Status::Ok(n) | Status::Error(s) wäre es Compile-Fehler.
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