Pattern Matching in Rust
Auf der Seite Match hast du die Grundlagen kennengelernt: Arme, _, Werte mit | gruppieren und Bereiche. Das ist aber erst der Anfang. Ein Pattern (Muster) kann in Rust auch in einen Wert hineinschauen, die Teile herausholen, die du brauchst, und zusätzliche Bedingungen prüfen – alles in einer Zeile. Genau das meint Pattern Matching (Musterabgleich).
Stell dir ein Muster wie eine Ausstechform für Plätzchen vor. Du drückst sie auf einen Wert. Passt die Form, gibt Rust dir die Teile darin. Passt sie nicht, probiert Rust den nächsten Arm. Wenn das einmal sitzt, benutzt du Muster überall: in match, in let, in Funktionsparametern und in if let.
Match Guards: ein zusätzliches if
Abschnitt betitelt „Match Guards: ein zusätzliches if“Manchmal reicht die Form allein nicht, und du brauchst noch eine weitere Prüfung. Ein Match Guard (Zusatzbedingung) ist eine if-Bedingung, die du hinter das Muster schreibst. Der Arm läuft nur, wenn das Muster passt und die Bedingung wahr ist.
fn main() { let temperatures = [-5, 0, 18, 35];
for t in temperatures { let feeling = match t { t if t < 0 => "eiskalt", 0 => "genau null", t if t < 25 => "angenehm", _ => "heiß", }; println!("{t} °C ist {feeling}"); }}Ausgabe:
-5 °C ist eiskalt0 °C ist genau null18 °C ist angenehm35 °C ist heißRust prüft die Arme von oben nach unten, die Reihenfolge ist also wichtig. 18 scheitert am ersten Guard, ist nicht 0 und erfüllt t < 25 – also ist es „angenehm“.
Tupel und Structs destrukturieren
Abschnitt betitelt „Tupel und Structs destrukturieren“Destrukturieren heißt, einen Wert in seine Einzelteile zu zerlegen. Bei einem Tupel schreibst du das Muster in derselben Form wie das Tupel. Bei einem Struct nennst du die Felder, die du haben willst.
struct Point { x: i32, y: i32,}
fn main() { let pair = (3, -7); match pair { (0, 0) => println!("der Ursprung"), (x, 0) => println!("auf der x-Achse bei {x}"), (0, y) => println!("auf der y-Achse bei {y}"), (x, y) => println!("irgendwo anders: ({x}, {y})"), }
let p = Point { x: 0, y: 5 }; match p { Point { x: 0, y } => println!("auf der y-Achse, y = {y}"), Point { x, y: 0 } => println!("auf der x-Achse, x = {x}"), Point { x, y } => println!("bei ({x}, {y})"), }}Ausgabe:
irgendwo anders: (3, -7)auf der y-Achse, y = 5Achte auf die Mischung. 0 ist ein fester Wert, der genau passen muss. x und y sind neue Variablen, die auffangen, was auch immer dort steht. Point { x, y } ist die Kurzform von Point { x: x, y: y }.
Enums mit Daten destrukturieren
Abschnitt betitelt „Enums mit Daten destrukturieren“Hier spielen Muster ihre Stärke richtig aus. Eine Variante eines Enums kann Daten mitbringen. Ein Muster kann diese Daten auspacken und gleichzeitig prüfen, welche Variante du vor dir hast.
enum Shape { Circle { radius: f64 }, Rectangle { width: f64, height: f64 }, Square(f64),}
fn area(shape: &Shape) -> f64 { match shape { Shape::Circle { radius } => std::f64::consts::PI * radius * radius, Shape::Rectangle { width, height } => width * height, Shape::Square(side) => side * side, }}
fn main() { let shapes = [ Shape::Circle { radius: 2.0 }, Shape::Rectangle { width: 3.0, height: 4.5 }, Shape::Square(1.5), ]; for shape in &shapes { println!("Fläche = {:.2}", area(shape)); }}Ausgabe:
Fläche = 12.57Fläche = 13.50Fläche = 2.25An radius kommst du nur heran, wenn du vorher nachgewiesen hast, dass der Wert ein Circle ist. Genau das ist der Sicherheitsgewinn: Der Compiler lässt dich keine Daten aus der falschen Variante lesen.
Verschachtelte Muster und @-Bindungen
Abschnitt betitelt „Verschachtelte Muster und @-Bindungen“Muster lassen sich in andere Muster stecken, so tief du willst. Hier enthält ein Command-Enum ein Direction-Enum, und ein Arm gleicht beide Ebenen auf einmal ab.
Mit einer @-Bindung prüfst du einen Wert gegen ein Muster und behältst ihn gleichzeitig in einer Variablen. n @ 1..=9 bedeutet: „Prüfe, ob der Wert zwischen 1 und 9 liegt, und nenne ihn n.“
enum Direction { Up, Down,}
enum Command { Move(Direction, u32), Say(String), Quit,}
fn run(cmd: Command) { match cmd { Command::Move(Direction::Up, steps @ 1..=9) => println!("hoch {steps} (kleiner Schritt)"), Command::Move(Direction::Up, steps) => println!("hoch {steps} (großer Sprung!)"), Command::Move(Direction::Down, steps) => println!("runter {steps}"), Command::Say(text) => println!("sage: {text}"), Command::Quit => println!("tschüss"), }}
fn main() { run(Command::Move(Direction::Up, 3)); run(Command::Move(Direction::Up, 50)); run(Command::Move(Direction::Down, 2)); run(Command::Say(String::from("hallo"))); run(Command::Quit);}Ausgabe:
hoch 3 (kleiner Schritt)hoch 50 (großer Sprung!)runter 2sage: hallotschüssOhne @ prüft ein Bereich wie 1..=9 den Wert, wirft ihn danach aber weg. Mit @ darfst du ihn behalten.
Teile ignorieren mit _ und ..
Abschnitt betitelt „Teile ignorieren mit _ und ..“Oft interessieren dich nur einige der Teile. Mit _ überspringst du einen einzelnen Wert. Mit .. überspringst du „alles andere“.
struct Player { name: String, level: u32, gold: u32,}
fn main() { let rgb = (255, 128, 0); let (red, _, _) = rgb; println!("red = {red}");
let numbers = (1, 2, 3, 4, 5); let (first, .., last) = numbers; println!("first = {first}, last = {last}");
let hero = Player { name: String::from("Mira"), level: 7, gold: 120 }; let Player { name, level, .. } = hero; println!("{name} ist auf Level {level}");}Ausgabe:
red = 255first = 1, last = 5Mira ist auf Level 7.. darf in einem Muster nur einmal vorkommen. (first, .., last) ist in Ordnung, (.., middle, ..) aber nicht – Rust könnte nicht erkennen, welchen Wert du meinst.
Muster in let und in Funktionsparametern
Abschnitt betitelt „Muster in let und in Funktionsparametern“Jedes let ist heimlich ein Muster. Jeder Funktionsparameter ebenfalls. Du kannst einen Wert also genau dort destrukturieren, wo er ankommt.
fn print_coords(&(x, y): &(i32, i32)) { println!("x: {x}, y: {y}");}
fn main() { let (width, height) = (1920, 1080); println!("{width}x{height}");
print_coords(&(4, -2));
let points = vec![(1, 2), (3, 4), (5, 6)]; let sums: Vec<i32> = points.iter().map(|&(a, b)| a + b).collect(); println!("{:?}", sums);}Ausgabe:
1920x1080x: 4, y: -2[3, 7, 11]Das Closure |&(a, b)| a + b nutzt dieselbe Idee. Jedes Element ist ein &(i32, i32), und das Muster packt es direkt in a und b aus. Das wirst du bei Iteratoren oft sehen.
matches! und let-else
Abschnitt betitelt „matches! und let-else“Zwei praktische Werkzeuge ersparen dir ein vollständiges match.
matches!(value, pattern) liefert true oder false. Das ist ideal in einem if oder in .filter(), wenn du nur eine Ja/Nein-Antwort brauchst. Guards funktionieren hier auch.
let-else erledigt „Pack das aus – oder verschwinde“. Du schreibst let MUSTER = wert else { ... };. Passt das Muster, stehen die Variablen im Rest der Funktion zur Verfügung. Passt es nicht, läuft der else-Block. Und der muss aussteigen – mit return, break, continue oder einer Panic.
fn parse_age(input: &str) -> Option<u32> { let Ok(age) = input.parse::<u32>() else { println!("'{input}' ist keine Zahl"); return None; }; Some(age)}
fn main() { let grade = 'B'; println!("{}", matches!(grade, 'A' | 'B'));
let scores = [45, 72, 90, 38, 66]; let passed = scores.iter().filter(|s| matches!(s, 50..=100)).count(); println!("{passed} bestanden");
println!("{:?}", parse_age("31")); println!("{:?}", parse_age("einunddreißig"));}Ausgabe:
true3 bestandenSome(31)'einunddreißig' ist keine ZahlNoneMit let-else bleibt der „Happy Path“ (der Normalfall) flach. Statt deinen Code in if let ... { } zu verschachteln, kümmerst du dich zuerst um den Fehlerfall und machst dann einfach weiter. Das passt gut zu Werten vom Typ Option und Result.
Häufige Fehler
Abschnitt betitelt „Häufige Fehler“Guards zählen nicht, wenn Rust prüft, ob alle Fälle abgedeckt sind. Der Compiler kann deine if-Bedingungen nicht auswerten. Deshalb schlägt das hier fehl, obwohl die beiden Guards zusammen jede Zahl abdecken:
fn main() { let n: i32 = 7; match n { x if x < 0 => println!("negativ"), x if x >= 0 => println!("null oder positiv"), }}error[E0004]: non-exhaustive patterns: `i32::MIN..=i32::MAX` not covered --> src/main.rs:3:11 |3 | match n { | ^ pattern `i32::MIN..=i32::MAX` not covered | = note: the matched value is of type `i32` = note: match arms with guards don't count towards exhaustivityDer Compiler sagt: Die Muster sind nicht vollständig („non-exhaustive“), und Arme mit Guards zählen dabei nicht mit. Die Lösung: Mach aus dem letzten Arm einen einfachen Catch-all, zum Beispiel _ => println!("null oder positiv").
Ein Variablenname in einem Muster ist eine neue Variable, kein Vergleich. Das hier sieht so aus, als würde es guess == expected prüfen – tut es aber nicht:
fn main() { let expected = 3; let guess = 5; match guess { expected => println!("Richtig!"), _ => println!("Falsch"), }}Der Code kompiliert und gibt Richtig! aus. Denn expected im Arm legt einfach eine neue Variable an, die jeden Wert auffängt. Rust warnt dich davor:
warning: unreachable pattern --> src/main.rs:6:9 |5 | expected => println!("Richtig!"), | -------- matches any value6 | _ => println!("Falsch"), | ^ no value can reach thisDie Warnung bedeutet: Der zweite Arm ist unerreichbar, weil expected schon auf jeden Wert passt. Die Lösung ist ein Guard: g if g == expected => println!("Richtig!").
Übungen
Abschnitt betitelt „Übungen“1. FizzBuzz mit einem Tupel
Abschnitt betitelt „1. FizzBuzz mit einem Tupel“Gib die Zahlen von 9 bis 15 aus. Für Vielfache von 3 gibst du stattdessen Fizz aus, für Vielfache von 5 Buzz und für Vielfache von beiden FizzBuzz. Erledige das mit einem match auf ein Tupel statt mit einer if-Kette. Tipp: Gleiche (n % 3, n % 5) ab. Ein Rest von 0 heißt „geht glatt auf“, und _ heißt „egal“.
Lösung anzeigen
fn main() { for n in 9..=15 { match (n % 3, n % 5) { (0, 0) => println!("FizzBuzz"), (0, _) => println!("Fizz"), (_, 0) => println!("Buzz"), _ => println!("{n}"), } }}Ausgabe:
FizzBuzz11Fizz1314FizzBuzz(0, 0) muss zuerst kommen. Stünde (0, _) oben, würde bei 15 Fizz ausgegeben.
2. Kontobewegungen
Abschnitt betitelt „2. Kontobewegungen“Definiere enum Transaction { Deposit(u32), Withdraw(u32), Fee }. Starte mit einem balance (Kontostand) von 100 und wende dann diese Liste der Reihe nach an: 50 einzahlen, 30 abheben, 500 abheben, Gebühr. Eine Gebühr kostet 5. Eine Abhebung, die größer als der Kontostand ist, wird mit einer Meldung abgelehnt. Gib am Ende den Kontostand aus. Tipp: Nimm einen Match Guard, Transaction::Withdraw(amount) if amount <= balance, und danach einen zweiten Withdraw-Arm für den abgelehnten Fall.
Lösung anzeigen
enum Transaction { Deposit(u32), Withdraw(u32), Fee,}
fn main() { let mut balance: u32 = 100; let history = [ Transaction::Deposit(50), Transaction::Withdraw(30), Transaction::Withdraw(500), Transaction::Fee, ];
for t in history { match t { Transaction::Deposit(amount) => balance += amount, Transaction::Withdraw(amount) if amount <= balance => balance -= amount, Transaction::Withdraw(amount) => println!("abgelehnt: {amount} kann nicht abgehoben werden"), Transaction::Fee => balance -= 5, } }
println!("Kontostand = {balance}");}Ausgabe:
abgelehnt: 500 kann nicht abgehoben werdenKontostand = 115Der zweite Withdraw-Arm hat keinen Guard. Er fängt also jede Abhebung auf, die der erste Arm abgelehnt hat. Ohne ihn würde das match nicht kompilieren: Guards zählen nicht, wenn Rust prüft, ob jeder Fall abgedeckt ist.
Zusammenfassung
Abschnitt betitelt „Zusammenfassung“- Ein Match Guard (
muster if bedingung) fügt einem Arm eine zusätzliche Prüfung hinzu. - Muster können Tupel, Structs und Enum-Varianten destrukturieren, und du kannst sie verschachteln.
name @ musterprüft einen Wert und behält ihn gleichzeitig in einer Variablen._überspringt einen Wert,..den Rest. Muster funktionieren auch inlet, in Funktionsparametern und in Closures.matches!liefert schnelltrueoderfalse; let-else packt einen Wert aus oder steigt früh aus.
Kurzer Check
Abschnitt betitelt „Kurzer Check“Kurzer Check
1. Was macht n @ 1..=9 in einem match-Arm?
2. Was muss der else-Block bei let-else tun?
3. Was ergibt let (first, .., last) = (1, 2, 3, 4, 5);?
4. Warum lässt sich ein match, das nur Arme mit Guards hat, oft nicht kompilieren?
Punkte: 0 / 4
Weiterlesen
Abschnitt betitelt „Weiterlesen“- Muster und Pattern Matching – Das Rust-Buch: das offizielle Kapitel über alle Stellen, an denen Muster vorkommen können, und wann sie nicht passen.
- Mustersyntax – Das Rust-Buch: ein Rundgang durch alle Musterformen, von Bereichen und Destrukturieren bis zu Guards und
@-Bindungen. - Auf dem richtigen Weg bleiben mit let…else – Das Rust-Buch: ein behutsames, durchgearbeitetes Beispiel für let-else.
- Destructuring – Rust by Example (auf Englisch): ausführbare Beispiele zum Destrukturieren von Tupeln, Arrays, Enums, Zeigern und Structs.
- Das Makro
matches!– Doku der Standardbibliothek (auf Englisch): die genaue Syntax des Makros, auch wie du einen Guard ergänzt.