Mastering Pattern Matching in Rust
Pattern Matching is Rust's powerful control flow construct that allows you to compare values against patterns and execute code based on which pattern matches. It provides exhaustive checking (ensuring all cases are handled), destructuring capabilities, and works seamlessly with enums, structs, and primitive types.
1. Basic Match Expressions
The match expression is Rust's most comprehensive pattern matching tool. It compares a value against a series of patterns and executes the code of the first matching arm.
fn main() {
let number = 3;
match number {
1 => println!("One"), // Exact value matching
2 | 3 | 5 | 7 => println!("Prime"), // Multiple values
4 | 6 | 8 | 9 => println!("Composite"),
_ => println!("Out of range"), // Default catch-all
}
// Match can return values
let description = match number {
1 => "unity",
2 => "binary",
_ => "other", // Must cover all possibilities
};
}
Match Quiz
What happens if you remove the `_` case in this match?
match some_u8_value {
0 => "zero",
1 => "one",
}
Key Points: The _ pattern is required when not all cases are explicitly handled. Match arms are checked in order from top to bottom. Match expressions must be exhaustive for enums.
2. Destructuring Values
Destructuring allows you to break down complex data types into their components. Rust can destructure tuples, structs, enums, and references.
// Destructuring tuples
let pair = (0, -2);
match pair {
(0, y) => println!("First is 0, y = {}", y), // Match first element
(x, 0) => println!("x = {}, second is 0", x),
_ => println!("No zeros"), // Catch remaining cases
}
// Destructuring structs
struct Point { x: i32, y: i32 }
let p = Point { x: 0, y: 7 };
match p {
Point { x, y: 0 } => println!("On x axis at {}", x),
Point { x: 0, y } => println!("On y axis at {}", y),
Point { x, y } => println!("At ({}, {})", x, y), // Full destructure
}
Destructuring Quiz
What does this match arm do?
Point { x: a @ 0..=5, y } => println!("Special x: {}", a)
Destructuring Tips: Use @ to both match and bind a value. The .. pattern ignores remaining fields in structs. Nested destructuring works for complex types.
3. Matching Enums
Enums and match are a perfect combination in Rust. The compiler ensures you handle all possible variants, making your code more robust.
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn process_message(msg: Message) {
match msg {
Message::Quit => println!("Quit"), // Unit variant
Message::Move { x, y } => { // Struct-like variant
println!("Move to x: {}, y: {}", x, y)
},
Message::Write(text) => println!("Text: {}", text), // Tuple variant
Message::ChangeColor(r, g, b) => { // Tuple variant
println!("Color: {}, {}, {}", r, g, b)
},
}
}
Enum Quiz
What's the advantage of matching enums vs using if-let chains?
Enum Matching Benefits: Exhaustiveness checking prevents bugs. You can access inner data directly in match arms. Complex enums can be nested and matched recursively.
4. Pattern Guards
Pattern guards add extra conditions to match arms using if expressions. They let you express more complex logic than patterns alone.
let num = Some(4);
match num {
Some(x) if x < 5 => println!("Less than 5: {}", x), // With condition
Some(x) => println!("{}", x), // Catch remaining Some values
None => (), // Handle None case
}
// Combining guards with patterns
match some_value {
(x, y) if x == y => println!("Equal"), // Check equality
(x, y) if x + y == 0 => println!("Additive inverse"),
_ => println!("No special case"), // Default
}
Guard Quiz
When would you use a pattern guard instead of nested matching?
Guard Usage: Guards are evaluated after pattern matching. They can reference bound variables from the pattern. Complex conditions often benefit from guards.
5. Advanced Patterns
Advanced pattern matching includes range matching, binding subpatterns, and ignoring values. These techniques make patterns more expressive.
// @ bindings combine matching and binding
match age {
age @ 0..=12 => println!("Child (age {})", age), // Bind and match
age @ 13..=19 => println!("Teen (age {})", age),
age => println!("Adult (age {})", age),
}
// Matching ranges (inclusive on both ends)
match number {
1..=5 => println!("1-5"), // Inclusive range
6..=10 => println!("6-10"),
_ => println!("Other"),
}
// Ignoring parts with _
let triple = (1, -2, 3);
match triple {
(x, _, z) => println!("First: {}, Last: {}", x, z), // Ignore middle
}
Advanced Quiz
What does `_` signify in patterns?
Advanced Features: .. ignores remaining fields in structs. | matches multiple patterns in one arm. Ranges work with chars and integers.
6. Concise Matching with if-let/while-let
if-let and while-let provide more concise syntax for cases where you only care about one pattern. They're syntactic sugar for match expressions.
// if-let for single pattern matching
let config_max = Some(3u8);
if let Some(max) = config_max { // Extract and match
println!("Max configured: {}", max);
}
// while-let for conditional loops
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() { // Continue while Some
println!("{}", top);
}
// Combining with else
if let Some(3) = config_max {
println!("Three!");
} else { // Optional else clause
println!("Not three");
}
if-let Quiz
When should you prefer if-let over match?
Usage Guidelines: Use if-let for simple optional checks. while-let is great for iterator-like processing. Prefer match when you need exhaustiveness.