Pattern-Based Branching
Beyond testing simple boolean conditions with if, Rust provides pattern-based branching constructs—match, if let, let ... else, while let, and the matches! macro—that simultaneously test the shape of a value and unpack its inner fields into local variables.
Overview of Pattern-Branching Constructs
| Construct | Exhaustive? | Best Used When |
|---|---|---|
match value { ... } |
Yes (compiler forces handling every possible case) | Branching across 2 or more variants, ranges, or structural patterns |
if let PAT = expr { ... } |
No (handles 1 target pattern; optional else) |
Acting on a single pattern (such as Some(x)) and ignoring the rest |
let PAT = expr else { diverge }; |
No (the else block must diverge: return, break, continue, panic!) |
Unwrapping a pattern into the current scope with a flat early-return guard |
while let PAT = expr { ... } |
No (loops as long as expr matches PAT) |
Repeatedly pulling items from an iterator, stack (.pop()), or channel |
matches!(expr, PAT) |
No (returns true or false as a bool) |
Testing whether a value matches a pattern inside a boolean condition or .filter() |
Exhaustive Multi-Way Branching with match
A match expression compares a value against a top-to-bottom sequence of arms (PATTERN => EXPRESSION) and executes the first arm whose pattern matches:
fn describe_point(point: (i32, i32)) -> &'static str {
match point {
(0, 0) => "At the origin",
(x, 0) if x > 0 => "On the positive X-axis",
(_, 0) => "On the negative X-axis",
(0, _) => "On the Y-axis",
(x, y) if x == y => "On the main diagonal (x == y)",
_ => "In a general quadrant",
}
}
fn main() {
for pt in [(0, 0), (5, 0), (0, -3), (4, 4), (2, 7)] {
println!("{pt:?} -> {}", describe_point(pt));
}
}
(0, 0) -> At the origin
(5, 0) -> On the positive X-axis
(0, -3) -> On the Y-axis
(4, 4) -> On the main diagonal (x == y)
(2, 7) -> In a general quadrant
Rich Pattern Capabilities in match
- Literal and Range Patterns (
1..=5): Match exact numbers, characters, or inclusive ranges. - Or-Patterns (
|): Match any of several patterns in a single arm ('a' | 'e' | 'i' | 'o' | 'u' => "vowel"). - Destructuring Patterns: Unpack tuples
(a, b), arrays[first, .., last], structsPoint { x, y }, and enum payloadsSome(v). - Match Guards (
if condition): Add an extra boolean filter after a pattern (Some(n) if n % 2 == 0 => ...). @Bindings (id @ 100..=199): Test a value against a sub-pattern or range while simultaneously binding the matched value to a variableid!- Wildcard (
_) and Catch-All Identifier (other): Ensures thematchis exhaustive (E0004if any possible value is left uncovered).
fn inspect_code(code: u16) {
match code {
200 | 204 => println!("Standard success ({code})"),
redirect @ 300..=399 => println!("Redirect code bound via @: {redirect}"),
other => println!("Other code: {other}"),
}
}
fn main() {
inspect_code(200);
inspect_code(301);
inspect_code(500);
}
Standard success (200)
Redirect code bound via @: 301
Other code: 500
Single-Pattern Branching with if let
When you only care about one specific pattern and want to ignore all other cases, a full match with _ => {} is verbose. if let PATTERN = EXPRESSION { ... } runs its block only when the value matches PATTERN:
fn main() {
let config_port: Option<u16> = Some(8080);
// Unpacks `p` only if `config_port` is `Some(p)`:
if let Some(p) = config_port {
println!("Listening on configured port {p}");
} else {
println!("Using fallback port 3000");
}
}
Listening on configured port 8080
Flat Unwrapping with let ... else
Introduced in Rust 1.65, a let ... else statement binds pattern variables directly in the current outer scope (avoiding right-ward indentation drift), and requires an else { ... } block that diverges (return, break, continue, or panic!) if the pattern does not match:
if let (Indents Happy Path): let ... else (Keeps Happy Path Flat!):
if let Some(val) = opt { let Some(val) = opt else {
// `val` only in scope inside `{}` return; // Must diverge!
do_work(val); };
} // `val` is now in scope for the rest of the function!
do_work(val);
fn parse_header_pair(line: &str) -> Option<(&str, u32)> {
// 1. Unpack `Some((key, raw_val))` into the function scope, or return `None` early:
let Some((key, raw_val)) = line.split_once(':') else {
return None;
};
// 2. Unpack `Ok(number)` into the function scope, or return `None` early:
let Ok(number) = raw_val.trim().parse::<u32>() else {
return None;
};
Some((key.trim(), number))
}
fn main() {
println!("{:?}", parse_header_pair("Content-Length: 512"));
println!("{:?}", parse_header_pair("MalformedHeader"));
}
Some(("Content-Length", 512))
None
Conditional Pattern Loops with while let
while let PATTERN = EXPRESSION { ... } repeatedly evaluates EXPRESSION and runs the loop body for as long as the result matches PATTERN:
fn main() {
let mut stack = vec![10, 20, 30];
// `stack.pop()` returns `Some(top)` until the vector is empty (`None`):
while let Some(top) = stack.pop() {
println!("Popped: {top}");
}
}
Popped: 30
Popped: 20
Popped: 10
Testing Patterns as a bool with matches!
The standard library macro matches!(expression, pattern) evaluates to true if expression matches pattern (including | alternatives and if guards) and false otherwise:
fn main() {
let ch = '7';
let is_hex_char = matches!(ch, '0'..='9' | 'a'..='f' | 'A'..='F');
let readings = [Some(5), None, Some(15), Some(-2)];
let positive_count = readings
.iter()
.filter(|&&item| matches!(item, Some(n) if n > 0))
.count();
println!("'{ch}' is hex: {is_hex_char}, positive readings: {positive_count}");
}
'7' is hex: true, positive readings: 2