Pattern Matching
Pattern matching is one of Rust's most powerful features. You got a taste of match in chapters 3 and 6 -- now we go
deep. By the end of this chapter, you will use pattern matching fluently to destructure data, handle variants, and write
expressive conditional logic.
match is exhaustive
The match expression must cover every possible value of the type being matched. The compiler enforces this:
enum TrafficLight {
Red,
Yellow,
Green,
}
fn action(light: &TrafficLight) -> &str {
match light {
TrafficLight::Red => "stop",
TrafficLight::Yellow => "caution",
TrafficLight::Green => "go",
}
}
fn main() {
let light = TrafficLight::Green;
println!("{}", action(&light));
}
If you remove one arm, the compiler rejects it:
error[E0004]: non-exhaustive patterns: `TrafficLight::Green` not covered
This is a safety feature -- you can never forget to handle a case. If you add a new variant to an enum, the compiler
tells you every match that needs updating.
The wildcard pattern _
When you do not want to list every case, use _ as a catch-all:
fn describe_number(n: i32) -> &'static str {
match n {
0 => "zero",
1 => "one",
2 => "two",
_ => "many",
}
}
fn main() {
println!("{}", describe_number(0)); // zero
println!("{}", describe_number(42)); // many
}
_ matches anything and is used as the last arm. If you want to bind the value without using it, prefix with _:
fn main() {
let pair = (1, 2);
match pair {
(0, _) => println!("First is zero"),
(_, 0) => println!("Second is zero"),
_ => println!("Neither is zero"),
}
}
Destructuring
Patterns can pull apart (destructure) structs, enums, tuples, and references.
Destructuring tuples
fn main() {
let point = (3, -7);
match point {
(0, 0) => println!("Origin"),
(x, 0) => println!("On x-axis at {x}"),
(0, y) => println!("On y-axis at {y}"),
(x, y) => println!("Point at ({x}, {y})"),
}
}
Destructuring structs
struct Point {
x: i32,
y: i32,
}
fn main() {
let point = Point { x: 5, y: -3 };
match point {
Point { x: 0, y: 0 } => println!("Origin"),
Point { x, y: 0 } => println!("On x-axis at {x}"),
Point { x: 0, y } => println!("On y-axis at {y}"),
Point { x, y } => println!("({x}, {y})"),
}
}
You can also destructure with let:
struct Point {
x: i32,
y: i32,
}
fn main() {
let point = Point { x: 10, y: 20 };
let Point { x, y } = point;
println!("x = {x}, y = {y}");
}
Destructuring enums
This is where pattern matching truly shines:
#[derive(Debug)]
enum Message {
Quit,
Echo(String),
Move { x: i32, y: i32 },
Color(u8, u8, u8),
}
fn process(msg: &Message) {
match msg {
Message::Quit => println!("Quitting"),
Message::Echo(text) => println!("Echo: {text}"),
Message::Move { x, y } => println!("Moving to ({x}, {y})"),
Message::Color(r, g, b) => println!("Color: rgb({r}, {g}, {b})"),
}
}
fn main() {
let messages = [
Message::Echo(String::from("hello")),
Message::Move { x: 10, y: 20 },
Message::Color(255, 128, 0),
Message::Quit,
];
for msg in &messages {
process(msg);
}
}
Nested destructuring
Patterns can be nested arbitrarily deep:
struct Point {
x: i32,
y: i32,
}
enum Shape {
Circle { center: Point, radius: f64 },
Rect { top_left: Point, bottom_right: Point },
}
fn describe(shape: &Shape) {
match shape {
Shape::Circle {
center: Point { x, y },
radius,
} => println!("Circle at ({x}, {y}) with radius {radius}"),
Shape::Rect {
top_left: Point { x: x1, y: y1 },
bottom_right: Point { x: x2, y: y2 },
} => println!("Rectangle from ({x1}, {y1}) to ({x2}, {y2})"),
}
}
fn main() {
let shape = Shape::Circle {
center: Point { x: 0, y: 0 },
radius: 5.0,
};
describe(&shape);
}
Multiple patterns with |
Use | (or) to match any of several patterns:
fn main() {
let number = 4;
match number {
1 | 2 => println!("one or two"),
3 | 4 => println!("three or four"),
_ => println!("something else"),
}
}
Match guards
Add an if condition to a match arm for extra filtering:
fn main() {
let number = 4;
match number {
n if n < 0 => println!("{n} is negative"),
n if n == 0 => println!("zero"),
n if n % 2 == 0 => println!("{n} is positive and even"),
n => println!("{n} is positive and odd"),
}
}
Guards are checked after the pattern matches. They do not count as exhaustive coverage, so you usually still need a catch-all arm.
Binding with @
The @ operator lets you bind a value to a name while also testing it against a pattern:
fn main() {
let age = 25;
match age {
n @ 0..=12 => println!("{n} -- child"),
n @ 13..=17 => println!("{n} -- teenager"),
n @ 18..=64 => println!("{n} -- adult"),
n @ 65.. => println!("{n} -- senior"),
_ => unreachable!(),
}
}
Without @, you would have to use a guard: 18..=64 if true => ... and then you would not have the value bound.
if let -- matching a single pattern
When you only care about one variant and want to ignore the rest, if let is more concise than match:
fn main() {
let maybe_number: Option<i32> = Some(42);
// With match
match maybe_number {
Some(n) => println!("Got {n}"),
None => {} // Do nothing
}
// Equivalent with if let
if let Some(n) = maybe_number {
println!("Got {n}");
}
}
if let is syntactic sugar for a match with one arm and a wildcard for everything else. You can add an else:
fn main() {
let config_value: Option<&str> = None;
if let Some(val) = config_value {
println!("Config: {val}");
} else {
println!("Using default config");
}
}
Use if let when:
- You only care about one variant
- A full
matchwould have an empty_ => {}arm - The code is more readable without listing every case
Use match when:
- You need to handle multiple variants
- You want the compiler to enforce exhaustiveness
while let -- looping on a pattern
while let loops as long as a pattern matches:
fn main() {
let mut stack = vec![1, 2, 3, 4, 5];
while let Some(top) = stack.pop() {
println!("{top}");
}
// Prints: 5, 4, 3, 2, 1
}
Vec::pop() returns Option<T> -- Some(value) when there are elements, None when empty. The while let keeps
going until pop() returns None.
let else -- early exit on pattern failure
let else (stabilized in Rust 1.65) lets you bind a pattern or diverge:
fn process_name(name: Option<&str>) {
let Some(name) = name else {
println!("No name provided");
return;
};
println!("Processing: {name}");
}
fn main() {
process_name(Some("Alice"));
process_name(None);
}
The else branch must diverge -- it must return, break, continue, or panic!. This is great for reducing
nesting when you want to "unwrap or bail out early".
The matches! macro
The matches! macro returns true if a value matches a pattern:
#[derive(Debug)]
enum Status {
Active,
Inactive,
Banned,
}
fn main() {
let status = Status::Active;
let is_active = matches!(status, Status::Active);
let is_restricted = matches!(status, Status::Inactive | Status::Banned);
println!("Active: {is_active}");
println!("Restricted: {is_restricted}");
}
This is a concise alternative to writing a match that returns true or false.
Matching on Option and Result
Since Option and Result are enums, pattern matching works naturally:
Option patterns
fn describe_option(opt: Option<i32>) {
match opt {
Some(0) => println!("Zero"),
Some(n) if n > 0 => println!("Positive: {n}"),
Some(n) => println!("Negative: {n}"),
None => println!("Nothing"),
}
}
fn main() {
describe_option(Some(42));
describe_option(Some(-3));
describe_option(Some(0));
describe_option(None);
}
Result patterns
fn parse_and_double(input: &str) -> Result<i32, String> {
match input.parse::<i32>() {
Ok(n) => Ok(n * 2),
Err(e) => Err(format!("Failed to parse '{input}': {e}")),
}
}
fn main() {
match parse_and_double("21") {
Ok(n) => println!("Result: {n}"),
Err(e) => println!("Error: {e}"),
}
match parse_and_double("abc") {
Ok(n) => println!("Result: {n}"),
Err(e) => println!("Error: {e}"),
}
}
Patterns in function parameters
Function parameters can be patterns too:
fn print_coordinates(&(x, y): &(i32, i32)) {
println!("({x}, {y})");
}
fn main() {
let point = (3, 5);
print_coordinates(&point);
}
A complete example
Here is a small command processor that demonstrates many pattern matching techniques:
#[derive(Debug)]
enum Command {
Set { key: String, value: String },
Get(String),
Delete(String),
List,
Exit,
}
fn execute(cmd: &Command) {
match cmd {
Command::Set { key, value } => {
println!("SET {key} = {value}");
}
Command::Get(key) | Command::Delete(key) => {
let action = if matches!(cmd, Command::Get(_)) {
"GET"
} else {
"DELETE"
};
println!("{action} {key}");
}
Command::List => println!("LIST all keys"),
Command::Exit => println!("Goodbye!"),
}
}
fn main() {
let commands = [
Command::Set {
key: String::from("name"),
value: String::from("Alice"),
},
Command::Get(String::from("name")),
Command::List,
Command::Delete(String::from("name")),
Command::Exit,
];
for cmd in &commands {
execute(cmd);
}
}
Summary
matchis exhaustive -- you must cover every possible value_is the wildcard that matches anything- Destructure tuples, structs, enums, and nested types in patterns
|matches multiple patterns; match guards addifconditions@binds a value while testing against a patternif letis concise for single-pattern matchingwhile letloops until a pattern no longer matcheslet elsebinds a pattern or diverges (early exit)matches!returns a boolean for quick pattern checks- Patterns work in
match,if let,while let,let, function parameters, andforloops
Next up: Collections -- Vec<T>, String, and HashMap<K, V> -- the standard library's
most-used data structures.