Why this matters
You have used patterns since module 2 without calling them that: let (a, b) = pair;, match arms, for (i, x) in ...enumerate(). Patterns are a small language for describing the shape of a value and pulling it apart in one step. Used well, they replace chains of field accesses, index checks and ifs with code that reads like a description of the data. This lesson collects the pattern features you will use constantly, plus the three control-flow forms built on them: if let, while let and let else.
Destructuring structs, tuples and enums
struct Point {
x: i32,
y: i32,
}
enum Event {
Click { pos: Point, button: u8 },
Key(char),
Resize(u32, u32),
}
fn main() {
let p = Point { x: 3, y: -7 };
let Point { x, y } = p;
println!("x={x} y={y}");
let events = [
Event::Click { pos: Point { x: 10, y: 0 }, button: 1 },
Event::Key('q'),
Event::Resize(800, 600),
Event::Click { pos: Point { x: 5, y: 5 }, button: 2 },
];
for e in &events {
match e {
Event::Click { pos: Point { x, y: 0 }, .. } => println!("click on top edge at x={x}"),
Event::Click { button: 2, .. } => println!("right click"),
Event::Click { .. } => println!("other click"),
Event::Key(c) => println!("key {c:?}"),
Event::Resize(w, h) => println!("resize to {w}x{h}"),
}
}
}
x=3 y=-7
click on top edge at x=10
key 'q'
resize to 800x600
right click
let Point { x, y } = p;takes a struct apart into variables named after its fields.Point { x: a, y: b }would bind them to other names.- Patterns nest:
Event::Click { pos: Point { x, y: 0 }, .. }matches a click whose position has y equal to 0, binding x. ..ignores the remaining fields (or tuple elements:(first, ..),(.., last)).- Literals inside patterns (
y: 0,button: 2) are tests.
Guards, ranges, alternatives and @
fn classify(n: i64) -> String {
match n {
i64::MIN..=-1 => String::from("negative"),
0 => String::from("zero"),
x @ (1 | 2 | 3 | 5 | 7) => format!("small prime {x}"),
x @ 1..=9 => format!("digit {x}"),
x if x % 2 == 0 => format!("big even {x}"),
x => format!("big odd {x}"),
}
}
fn main() {
for n in [-4, 0, 5, 8, 42, 99] {
println!("{}", classify(n));
}
}
negative
zero
small prime 5
digit 8
big even 42
big odd 99
a..=brange patterns work for integers andchars.p1 | p2matches either.name @ patternbinds the value while also testing it against a pattern.pattern if conditionadds a guard; guards are not considered for exhaustiveness.
Binding modes: what do the bindings hold?
When you match on a reference (like &events in a for loop, or &self in a method), the bindings become references automatically: c in Event::Key(c) above is a &char. This is called default binding mode, and it is why matching a borrowed value never moves anything out of it. When you match on an owned value, bindings take the fields by value (moving non-Copy ones). You will see &x in patterns too: it dereferences, as in for &v in values.iter().
if let: one pattern, one branch
When you only care about one variant, a full match is noisy. if let runs a block only if a pattern matches:
fn main() {
let words = ["apple", "x", "banana"];
for w in words {
if let Some(c) = w.chars().nth(1) {
println!("{w}: second char {c}");
} else {
println!("{w}: too short");
}
}
let config_port: Option<u16> = Some(8080);
if let Some(port) = config_port {
println!("listening on {port}");
}
}
apple: second char p
x: too short
banana: second char a
listening on 8080
if let PATTERN = EXPR { ... } else { ... } is a match with one interesting arm and a catch-all. (Option, with its Some(value) and None variants, is the subject of the next module.)
while let: loop while a pattern matches
fn main() {
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
println!("popped {top}");
}
println!("empty: {}", stack.is_empty());
}
popped 3
popped 2
popped 1
empty: true
The loop continues as long as stack.pop() returns Some, and stops at the first None. It is the natural way to drain a stack, a queue or any "get next thing until there is none" source.
let else: bind or bail out
Often a function must extract something or give up. let PATTERN = EXPR else { DIVERGE }; binds the pattern's variables for the rest of the block, or runs the else block, which must leave (return, break, continue or panic):
fn parse_pair(s: &str) -> Option<(i32, i32)> {
let Some((a, b)) = s.split_once(',') else {
return None;
};
let Ok(a) = a.trim().parse::<i32>() else {
return None;
};
let Ok(b) = b.trim().parse::<i32>() else {
return None;
};
Some((a, b))
}
fn main() {
for s in ["3, 4", "nope", "1,x"] {
println!("{s:?} -> {:?}", parse_pair(s));
}
}
"3, 4" -> Some((3, 4))
"nope" -> None
"1,x" -> None
Each step either succeeds and binds names at the top level, or exits immediately. The happy path stays unindented, unlike nested if lets. (parse returns Result, whose success variant is Ok; next module.)
matches! for yes/no questions
matches!(value, pattern) returns true if the pattern matches:
fn main() {
let c = 'k';
println!("{}", matches!(c, 'a'..='z' | 'A'..='Z'));
let n = 15;
println!("{}", matches!(n, x if x % 5 == 0));
}
true
true
Refutable and irrefutable patterns
A pattern that always matches, like (a, b) for a tuple or a plain name, is irrefutable. let and function parameters need irrefutable patterns. A pattern that can fail, like Some(x), is refutable and needs match, if let, while let or let else:
fn main() {
let maybe: Option<i32> = Some(3);
let Some(x) = maybe;
println!("{x}");
}
error[E0005]: refutable pattern in local binding
--> main.rs:3:9
|
3 | let Some(x) = maybe;
| ^^^^^^^ pattern `None` not covered
|
= note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
= note: for more information, visit https://doc.rust-lang.org/book/ch19-02-refutability.html
= note: the matched value is of type `Option<i32>`
help: you might want to use `let else` to handle the variant that isn't matched
|
3 | let Some(x) = maybe else { todo!() };
| ++++++++++++++++
E0005, with the compiler offering the let else fix.
Common mistakes
- A variable name where you meant a constant:
match x { limit => ... }binds a newlimitthat matches everything. Use a guard (v if v == limit). - Forgetting
..when ignoring struct fields: E0027 "pattern does not mention fields". Add... - Moving out of a borrowed value in a pattern: matching
*some_refby value and binding a String moves it: E0507. Match the reference instead, so bindings borrow. - Overusing
if letchains where amatchwith several arms would be clearer and exhaustive.
Recap
- Patterns destructure structs
S { a, b, .. }, tuples(x, ..), enumsE::V(x), and nest freely. - Literals and ranges test; names bind;
_and..ignore;|alternates;@binds and tests; guards add conditions. - Matching on a reference binds references (no moves).
if lethandles one pattern;while letloops while it matches;let elsebinds or diverges.letneeds irrefutable patterns (E0005); refutable ones needmatch,if letorlet else.
Going further: the Rust Book chapter 19 ("Patterns and Matching") lists every pattern form, including slice patterns like [first, .., last] and [x, rest @ ..]. Try those on an array.