Patterns and destructuring
In Rust a pattern is a description of the shape of a value. let uses one to unpack, match uses several to choose a branch, and both check the shape at compile time. This is the feature that replaces the visitor, the switch with fallthrough and half of the conditionals in an ordinary program.
The lesson starts with the simplest place a pattern appears, a let, then adds ranges, guards, bindings and enums. The last section breaks exhaustiveness and reads the error, because that error is the feature: the compiler lists the cases the code forgot.
Patterns in a let
let (a, b) = (1, 2); unpacks a tuple, and let Point { x, y } = point; unpacks a struct by field name. The names become ordinary bindings, and the value is destructured at the same time.
The program below unpacks one tuple and one struct and prints four numbers. The struct is declared in the same file, and both patterns are irrefutable: they cannot fail, which is what a let requires.
struct Point {
x: i32,
y: i32,
}
fn main() {
let (a, b) = (1, 2);
let Point { x, y } = Point { x: 3, y: 4 };
println!("{a} {b} {x} {y}");
}
rustc --edition 2024 let_patterns.rs
./let_patterns
1 2 3 4
A pattern that can fail needs match or if let, because a let has no branch to take when the shape does not fit. The lesson on Option uses that rule for a value that may be absent.
match arms, ranges and guards
A match tries its arms in order and runs the first that fits. An arm may be a literal, a range such as 1..=9, or a pattern with a guard, which is an if after the pattern that may read the bound names.
The program classifies four numbers, and the order of the arms matters: zero is matched before the range, and the negative case has a guard because a range cannot express it. The wildcard arm catches the rest.
fn describe(n: i32) -> &'static str {
match n {
0 => "zero",
1..=9 => "small",
n if n < 0 => "negative",
_ => "large",
}
}
fn main() {
for n in [0, 7, -3, 42] {
println!("{n}: {}", describe(n));
}
}
rustc --edition 2024 match_arms.rs
./match_arms
0: zero
7: small
-3: negative
42: large
The compiler checks that the arms cover every value of the type, which the last section turns into an error. A guard is not counted for exhaustiveness, so an arm with a guard never makes the match complete on its own.
Binding with @
n @ 1..=9 matches the range and also binds the value to n, so the arm can use the number it matched. Without the binding the arm would only know that the value was in the range.
The program matches a seven and prints the bound name. The second arm has no pattern beyond a name, so it catches everything else and binds it too.
fn main() {
let value = 7;
match value {
n @ 1..=9 => println!("single digit: {n}"),
n => println!("other: {n}"),
}
}
rustc --edition 2024 binding.rs
./binding
single digit: 7
A binding without a pattern is an irrefutable pattern, and it is how a match writes its fallback arm. The same syntax appears in if let and while let, which the lesson on loops introduces.
Matching an enum
An enum with data is matched by variant, and the pattern binds the fields of that variant. This is the shape that replaces a class hierarchy with a virtual method: one value, a fixed set of variants, and a match that must handle each one.
The program below has a circle and a square, each carrying a number, and computes an area for both. The match has one arm per variant, and the printed values are rounded to two decimals by the format specification from the lesson on formatting.
enum Shape {
Circle(f64),
Square(f64),
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle(r) => 3.14159 * r * r,
Shape::Square(s) => s * s,
}
}
fn main() {
println!("{:.2} {:.2}", area(&Shape::Circle(2.0)), area(&Shape::Square(3.0)));
}
rustc --edition 2024 enum_match.rs
./enum_match
12.57 9.00
The lesson on enums with data builds a bigger version of this type, and the one on match instead of the visitor compares it with double dispatch.
The arm that is missing
A match must cover every value of the type it inspects. When an enum gains a variant, every match that does not handle it stops compiling with E0004, and the message names the variant that is not covered. That is the compiler maintaining the code for you.
The program below has three colours and matches only two. The error names the missing variant, the type it belongs to, and a suggested arm that would complete the match.
enum Color {
Red,
Green,
Blue,
}
fn name(c: Color) -> &'static str {
match c {
Color::Red => "red",
Color::Green => "green",
}
}
fn main() {
println!("{}", name(Color::Blue));
}
rustc --edition 2024 nonexhaustive_err.rs
error[E0004]: non-exhaustive patterns: `Color::Blue` not covered
--> nonexhaustive_err.rs:8:11
|
8 | match c {
| ^ pattern `Color::Blue` not covered
|
note: `Color` defined here
--> nonexhaustive_err.rs:1:6
|
1 | enum Color {
| ^^^^^
...
4 | Blue,
| ---- not covered
= note: the matched value is of type `Color`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
|
10 ~ Color::Green => "green",
11 ~ Color::Blue => todo!(),
|
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0004`.
Adding a wildcard arm would silence the error and lose the warning when a fourth variant arrives, which is why a match over an enum usually lists every variant instead of a catch-all.
Exercises
- Rewrite the FizzBuzz program from the lesson on loops with a match over (n % 3, n % 5). Which arms do you need?
- Add a third shape to the enum and let the compiler list the matches that need an arm.
- Match a tuple of two booleans and print a different line for each of the four combinations. Which arm would a wildcard hide?
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does a pattern describe?
Answer
The shape of a value
A pattern says what a value looks like, and the compiler checks the shape when the pattern is used.
-
What does a match do with its arms?
Answer
Runs the first arm that fits
Arms are tried in order, and the first matching pattern decides the branch.
-
What does
n @ 1..=9do?Answer
Matches the range and binds the value to n
The @ binding keeps the matched value available under a name inside the arm.
-
What does E0004 report?
Answer
A pattern that is missing an arm for a value
The match is not exhaustive, and the error names the variant or value that is not covered.
-
What does this program print?
struct Point { x: i32, y: i32, } fn main() { let (a, b) = (1, 2); let Point { x, y } = Point { x: 3, y: 4 }; println!("{a} {b} {x} {y}"); }Answer
1 2 3 4
The tuple binds a and b, the struct pattern binds x and y, and the print lists them in order.
-
What does this program print?
fn describe(n: i32) -> &'static str { match n { 0 => "zero", 1..=9 => "small", n if n < 0 => "negative", _ => "large", } } fn main() { for n in [0, 7] { println!("{n}: {}", describe(n)); } }Answer
0: zero / 7: small
Zero matches the first arm and seven falls into the range one to nine.
-
What does this program print?
fn main() { let value = 7; match value { n @ 1..=9 => println!("single digit: {n}"), n => println!("other: {n}"), } }Answer
single digit: 7
Seven fits the range, and the binding lets the arm print the value it matched.
-
What does this program print?
enum Shape { Circle(f64), Square(f64), } fn area(shape: &Shape) -> f64 { match shape { Shape::Circle(r) => 3.14159 * r * r, Shape::Square(s) => s * s, } } fn main() { println!("{:.2}", area(&Shape::Square(3.0))); }Answer
9.00
The square arm multiplies three by three, and the format prints two decimals.
-
What is the first line of the compiler output for this program?
enum Color { Red, Green, Blue, } fn name(c: Color) -> &'static str { match c { Color::Red => "red", Color::Green => "green", } } fn main() { println!("{}", name(Color::Blue)); }Answer
error[E0004]: non-exhaustive patterns:
Color::Bluenot coveredThe match leaves one variant out, and the error names it together with a suggested arm.
-
What does this program print?
fn main() { let pair = (true, false); match pair { (true, true) => println!("both"), (true, false) => println!("first"), (false, _) => println!("second"), } }Answer
first
The second arm matches true and false exactly, and the wildcard in the third covers the rest.
The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev