Pattern Matching in Depth
Prerequisites: Iterators
Level 1 used match on simple enums. Patterns can do much more: pull apart
structs, tuples and nested enums, test ranges, add extra conditions, and bind
names to the parts you care about. Patterns appear in match, if let,
while let, let, for, and function parameters.
Syntax
fn main() {
let value = 7;
let text = match value {
0 => "zero", // literal
1 | 2 => "one or two", // several alternatives
3..=9 => "a single digit", // inclusive range
n if n < 0 => "negative", // match guard: extra condition
_ => "big", // wildcard: anything else
};
println!("{text}");
let point = (3, -2);
let (x, _) = point; // destructure a tuple, ignore a part
let is_small = matches!(value, 0..=9); // true/false instead of a full match
println!("{x} {is_small}");
}
| Pattern | Meaning |
|---|---|
Point { x, y } |
destructure a struct into variables x and y |
Point { x: 0, .. } |
x must be 0; ignore the other fields |
(first, .., last) |
first and last items of a tuple |
[first, rest @ ..] |
first element of a slice and a sub-slice of the rest |
id @ 1..=99 |
test a pattern and bind the matched value to id |
Some(n) if n > 5 |
a guard: the arm matches only if the condition is true |
match must be exhaustive: every possible value has to be covered by some
arm. That is what makes adding a new enum variant safe — the compiler lists
every match that needs updating.
let-else
let PATTERN = value else { ... }; binds the pattern if it matches, and
otherwise runs the else block, which must leave the current scope (with
return, break, continue or a panic). It keeps the "happy path" unindented:
fn parse_pair(text: &str) -> Option<(i32, i32)> {
let Some((a, b)) = text.split_once(',') else {
return None;
};
let (Ok(a), Ok(b)) = (a.trim().parse(), b.trim().parse()) else {
return None;
};
Some((a, b))
}
Example
#[derive(Debug)]
enum Shape {
Circle { radius: f64 },
Rect { w: f64, h: f64 },
Triangle(f64, f64, f64),
}
enum Message {
Move { x: i32, y: i32 },
Write(String),
Paint(Shape),
Quit,
}
fn describe(msg: &Message) -> String {
match msg {
Message::Move { x: 0, y: 0 } => String::from("stay put"),
Message::Move { x, y: 0 } => format!("move horizontally by {x}"),
Message::Move { x, y } => format!("move to ({x}, {y})"),
Message::Write(text) if text.is_empty() => String::from("write nothing"),
Message::Write(text) => format!("write '{text}'"),
Message::Paint(Shape::Circle { radius }) => format!("paint a circle, r = {radius}"),
Message::Paint(Shape::Rect { w, h }) if w == h => format!("paint a {w}x{h} square"),
Message::Paint(shape @ Shape::Rect { .. }) => format!("paint {shape:?}"),
Message::Paint(Shape::Triangle(a, b, c)) => {
format!("paint a triangle, perimeter {}", a + b + c)
}
Message::Quit => String::from("quit"),
}
}
fn classify(numbers: &[i32]) -> String {
match numbers {
[] => String::from("empty"),
[only] => format!("just {only}"),
[first, .., last] if first == last => format!("starts and ends with {first}"),
[first, rest @ ..] => format!("{first} then {} more", rest.len()),
}
}
fn age_group(age: u32) -> String {
match age {
0 => String::from("newborn"),
child @ 1..=12 => format!("child aged {child}"),
teen @ 13..=19 => format!("teenager aged {teen}"),
adult => format!("adult aged {adult}"),
}
}
fn main() {
let messages = [
Message::Move { x: 0, y: 0 },
Message::Move { x: 5, y: 0 },
Message::Move { x: 1, y: 2 },
Message::Write(String::new()),
Message::Write(String::from("hello")),
Message::Paint(Shape::Circle { radius: 1.5 }),
Message::Paint(Shape::Rect { w: 2.0, h: 2.0 }),
Message::Paint(Shape::Rect { w: 2.0, h: 3.0 }),
Message::Paint(Shape::Triangle(3.0, 4.0, 5.0)),
Message::Quit,
];
for msg in &messages {
println!("{}", describe(msg));
}
println!("{}", classify(&[]));
println!("{}", classify(&[7]));
println!("{}", classify(&[4, 1, 4]));
println!("{}", classify(&[1, 2, 3, 4]));
for age in [0, 8, 16, 40] {
println!("{}", age_group(age));
}
let quits = messages.iter().filter(|m| matches!(m, Message::Quit)).count();
println!("quit messages: {quits}");
}
Expected output
stay put
move horizontally by 5
move to (1, 2)
write nothing
write 'hello'
paint a circle, r = 1.5
paint a 2x2 square
paint Rect { w: 2.0, h: 3.0 }
paint a triangle, perimeter 12
quit
empty
just 7
starts and ends with 4
1 then 3 more
newborn
child aged 8
teenager aged 16
adult aged 40
quit messages: 1
Arms are tried top to bottom, so more specific patterns must come before
general ones: Move { x: 0, y: 0 } has to be listed before Move { x, y }.
Common mistakes
- Leaving a case out. Removing the
Message::Quitarm giveserror[E0004]: non-exhaustive patterns: `&Message::Quit` not covered. Add the arm (or_if you really mean "anything else"). - Putting a catch-all arm first. Later arms become unreachable and the
compiler warns
unreachable pattern. - Forgetting that a guard doesn't count toward exhaustiveness. The compiler
can't prove
n if n >= 0andn if n < 0cover everything, so a final_arm is still required. - Writing a
let-elsewhoseelseblock doesn't leave the scope:error[E0308]: `else` clause of `let...else` does not diverge. - Expecting a variable name in a pattern to compare against an existing
variable.
match x { limit => ... }doesn't compare with alimitvariable in scope — it creates a new binding that matches everything. Use a guard:n if n == limit.
Practice
Write fn fizzbuzz(n: u32) -> String using a single match on the tuple
(n % 3, n % 5), and print the results for 1 through 15 on one line,
separated by spaces.
Show solution
fn fizzbuzz(n: u32) -> String {
match (n % 3, n % 5) {
(0, 0) => String::from("FizzBuzz"),
(0, _) => String::from("Fizz"),
(_, 0) => String::from("Buzz"),
_ => n.to_string(),
}
}
fn main() {
let line: Vec<String> = (1..=15).map(fizzbuzz).collect();
println!("{}", line.join(" "));
}
1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz