Why this matters
When a value can be one of many cases (a menu choice, a command, a day of the week, a key code), a long if/else if chain becomes hard to read and easy to get wrong. match lists the cases as a table, and the compiler checks that the table is complete: forget a case and the program does not compile. match is one of Rust's most important features. This lesson introduces it on numbers and tuples; module 7 shows its full power with enums and destructuring.
The shape of a match
fn main() {
let code = 404;
let meaning = match code {
200 => "OK",
301 | 302 => "redirect",
404 => "not found",
500..=599 => "server error",
_ => "something else",
};
println!("{code}: {meaning}");
}
404: not found
match value { arms }. Each arm ispattern => expression,.- The value is compared against each pattern from top to bottom; the first match wins, its expression is evaluated, and the rest are skipped. There is no fall-through.
301 | 302is an or-pattern: either value matches.500..=599is a range pattern (inclusive)._is the wildcard: it matches anything and binds nothing.matchis an expression. All arms must produce the same type, here&str.
An arm's expression can be a block when it needs several statements:
fn main() {
let choice = 2;
match choice {
1 => println!("starting"),
2 => {
println!("saving");
println!("saved");
}
_ => println!("unknown choice"),
}
}
saving
saved
(A comma after a block arm is optional.)
Exhaustiveness
A match must handle every possible value of the type. Leave a gap and rustc refuses:
fn main() {
let n: u8 = 7;
let size = match n {
0 => "none",
1..=9 => "a few",
10..=99 => "many",
};
println!("{size}");
}
error[E0004]: non-exhaustive patterns: `100_u8..=u8::MAX` not covered
--> main.rs:3:22
|
3 | let size = match n {
| ^ pattern `100_u8..=u8::MAX` not covered
|
= note: the matched value is of type `u8`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
|
6 ~ 10..=99 => "many",
7 ~ 100_u8..=u8::MAX => todo!(),
|
E0004 even tells you exactly which values are missing: 100 through 255. The compiler knows the full range of a u8 and checks your patterns against it. This becomes enormously valuable with enums (module 7): add a new variant to a type and the compiler points at every match in the program that does not handle it yet.
For numbers there are usually too many values to list, so a final _ arm handles "everything else". For a bool, true and false together are exhaustive.
Binding the value and adding guards
A plain name as a pattern matches anything and binds the value to that name, so the arm can use it. A guard, if condition after the pattern, adds an extra test:
fn classify(n: i32) -> String {
match n {
0 => String::from("zero"),
x if x < 0 => format!("negative ({x})"),
x if x % 2 == 0 => format!("even ({x})"),
x => format!("odd ({x})"),
}
}
fn main() {
for n in [0, -4, 6, 7] {
println!("{}", classify(n));
}
}
zero
negative (-4)
even (6)
odd (7)
The last arm, x, catches everything left, so the match is exhaustive. Note that the compiler does not look inside guards when checking exhaustiveness; if all your arms have guards, you still need a catch-all. (format! builds a String, as in module 1; String::from makes one from a literal.)
Matching several values at once
Put values in a tuple and match on its shape:
fn main() {
for (x, y) in [(0, 0), (3, 0), (0, -2), (1, 1)] {
let place = match (x, y) {
(0, 0) => "origin",
(_, 0) => "on the x axis",
(0, _) => "on the y axis",
_ => "elsewhere",
};
println!("({x}, {y}) is {place}");
}
}
(0, 0) is origin
(3, 0) is on the x axis
(0, -2) is on the y axis
(1, 1) is elsewhere
(_, 0) matches any tuple whose second element is 0. This style replaces nested ifs with a flat table that reads like a specification.
match on text
Strings can be matched against literals, which is perfect for simple commands:
fn main() {
for cmd in ["start", "stop", "jump"] {
match cmd {
"start" | "go" => println!("starting"),
"stop" => println!("stopping"),
other => println!("unknown command: {other}"),
}
}
}
starting
stopping
unknown command: jump
For a String read from input, match on line.trim(), which gives the &str form the literals compare with (module 6 explains the difference between String and &str).
How match compiles
The compiler turns a match into whatever is fastest: a chain of comparisons for a few arms, a binary search over ranges, or a jump table (an array of code addresses indexed by the value) for dense integer cases. You write the clear version and the compiler picks the efficient one. A long else if chain usually ends up the same, but match states your intent and gets the exhaustiveness check.
Common mistakes
Unreachable arms. A wildcard or broad pattern before specific ones hides them:
fn main() {
let n = 3;
match n {
_ => println!("anything"),
3 => println!("three"),
}
}
warning: unreachable pattern
--> main.rs:5:9
|
4 | _ => println!("anything"),
| - matches any value
5 | 3 => println!("three"),
| ^ no value can reach this
|
= note: `#[warn(unreachable_patterns)]` on by default
It is a warning, not an error, but it is always a bug. Put specific patterns first.
Arms with different types: E0308 "match arms have incompatible types". Every arm of a match used as a value must produce the same type.
Using a variable as if it were a constant. match n { limit => ... } does not compare with a variable called limit; it creates a new binding named limit that matches everything. To compare against a variable, use a guard: x if x == limit =>. (Constants declared with const do work as patterns.)
Recap
match value { pattern => expr, ... }tries arms in order; the first match wins; no fall-through.- Patterns so far: literals,
a | b, rangesa..=b,_, a binding name, tuples. - Guards
pattern if condadd conditions; they do not count toward exhaustiveness. - A match must be exhaustive (E0004 lists the missing values);
_catches the rest. matchis an expression; all arms need the same type.
Going further: rewrite the grade calculator from lesson 1 with a match on the score using range patterns, and see how the compiler reacts if you leave out a range. Module 7 continues the story with enums, destructuring structs and if let.