11.4match in depth
You met match for control flow in lesson 7.3 and have leaned on it for three enum lessons since. It's time to learn everything it can do, because match is the most powerful single construct in Rust, and the patterns you write in it are a small language of their own. This lesson is a tour of that language.
Binding patterns: naming what you match
You've already seen the central trick: a pattern can bind a name to the value it matched. Lesson 11.2's Message::Write(text) matched the variant and named its payload text. The same works for Option:
fn main() {
let config: Option<i32> = Some(42);
match config {
Some(value) => println!("got {value}"),
None => println!("nothing"),
}
}got 42
Some(value) only matches Some, and when it does, value is bound to the inner i32, usable on the right-hand side. The name is yours to choose; Some(n), Some(count), Some(x) all work. Binding is what makes match more than a fancy if: it checks the shape and extracts the contents in one move.
Multiple patterns with |
One arm can match several patterns at once with |, read "or". You saw this for vowels in lesson 7.3:
fn main() {
let n = 5;
match n {
1 | 3 | 5 | 7 | 9 => println!("odd single digit"),
0 | 2 | 4 | 6 | 8 => println!("even single digit"),
_ => println!("something else"),
}
}odd single digit
Each arm fires if any of its patterns match. This keeps related cases together instead of duplicating the same right-hand side across many arms.
Ranges with ..=
For matching a span of values, ..= (the inclusive range from lesson 7.5) works as a pattern:
fn grade(score: u32) -> char {
match score {
90..=100 => 'A',
80..=89 => 'B',
70..=79 => 'C',
60..=69 => 'D',
_ => 'F',
}
}
fn main() {
println!("{}", grade(85)); // B
println!("{}", grade(42)); // F
}B
F
90..=100 matches any value from 90 through 100 inclusive. Ranges in patterns must be inclusive (..=) or open-ended; the half-open .. isn't allowed here, because the compiler wants the boundaries unambiguous. Range patterns work on integers and chars ('a'..='z' matches any lowercase letter), which is often cleaner than a long | chain.
Match guards: an extra condition
Sometimes matching the shape isn't enough and you need an additional test. A match guard is an if condition tacked onto an arm; the arm fires only if the pattern matches and the guard is true:
fn main() {
let pair = (3, -3);
match pair {
(x, y) if x + y == 0 => println!("sum to zero"),
(x, _) if x % 2 == 0 => println!("first is even"),
_ => println!("no special property"),
}
}sum to zero
The first arm matches any pair, but only fires when x + y == 0. Guards let a pattern reach beyond pure structure to arbitrary logic, including comparisons between the bound values, which a plain pattern can't express. The cost is that the compiler can't reason about guards for exhaustiveness, so you'll usually still want a final catch-all.
The two catch-alls: _ and a name
When you don't need every case spelled out, two patterns match anything. You've used _, the wildcard, since lesson 7.3: it matches any value and binds nothing. The other is a plain name, which matches anything and binds it, so you can use the value:
fn main() {
let dice = 4;
match dice {
3 => println!("three: special"),
7 => println!("seven: special"),
other => println!("rolled a {other}"),
}
}rolled a 4
other catches every value 3 and 7 didn't, and names it so the arm can print it. Use _ when you want to ignore the value, a name when you want it. Both make a match exhaustive, which is why a catch-all is the usual way to close a match on a type with too many values to list (like a full i32).
Warning
Catch-all arms must come last. match tries arms top to bottom and takes the first that matches, so a _ or a bare name placed early would swallow everything below it. The compiler warns about unreachable arms (unreachable_pattern) when this happens, the same kind of dead-code warning you met in lesson 2.2. Specific patterns first, general patterns last.
Binding and testing at once with @
Occasionally you want to test a value against a range and keep the value. The @ operator binds a name to a value while also testing it against a pattern:
fn main() {
let id = 5;
match id {
small @ 1..=9 => println!("single digit: {small}"),
big @ 10..=99 => println!("double digit: {big}"),
_ => println!("big number"),
}
}single digit: 5
small @ 1..=9 says "match a value in 1 through 9, and bind it to small." Without @ you'd have to choose between testing the range (and losing the value) or binding the value (and losing the range test). The @ gives you both, which is exactly what its quiz-favorite reputation rests on.
Key insight
A pattern does two jobs that an if separates: it tests a value's shape and extracts the parts in one step. Some(n), 90..=100, (x, y) if x + y == 0, small @ 1..=9: each both decides whether the arm fires and names the pieces you'll use. That fusion is why match replaces long ladders of if-else and field access with a single readable block.
Quiz time
Question #1
What does this print, and why?
fn main() {
let n = 7;
match n {
x if x < 0 => println!("negative"),
0 => println!("zero"),
1..=9 => println!("small"),
_ => println!("big"),
}
}Show solution
small. The first arm's guard x < 0 is false (7 isn't negative), 0 doesn't match, and 1..=9 matches 7, so that arm fires and the _ is never reached. Arms are tried top to bottom; the first match wins.
Question #2
Rewrite this if-else chain as a single match with a range pattern and a catch-all:
fn fee(age: u32) -> u32 {
if age <= 5 {
0
} else if age <= 17 {
10
} else {
20
}
}Show solution
fn fee(age: u32) -> u32 {
match age {
0..=5 => 0,
6..=17 => 10,
_ => 20,
}
}
The ranges are inclusive (..=) and don't overlap; the _ covers 18 and up. Cleaner than the chain, and the compiler checks it covers every u32.
Question #3
Use @ to write a match arm that matches an i32 in the range 1 through 12, binds it to month, and prints "month N is valid". Add a catch-all for everything else.
Show solution
fn check(n: i32) {
match n {
month @ 1..=12 => println!("month {month} is valid"),
_ => println!("not a month"),
}
}
month @ 1..=12 tests the range and binds the value in one pattern, so the arm can both fire only for valid months and print which one.
These patterns work on more than enums. The next lesson aims the same machinery at structs and tuples: destructuring, which lets a single pattern pull apart a whole compound value at once.