Pattern Matching
Master match expressions, if let, while let, and destructuring in Rust.
The match Expression
The match expression is one of the most powerful control flow constructs in Rust. It compares a value against a series of patterns and executes the code associated with the first matching pattern. Match expressions must be exhaustive, meaning every possible value must be covered. The compiler enforces this, so you cannot accidentally forget to handle a case. Match arms can bind variables, use guards, and return values.
fn describe_number(n: i32) -> &'static str {
match n {
1 => "one",
2 => "two",
3..=9 => "between three and nine",
10 | 20 | 30 => "a multiple of ten",
n if n < 0 => "negative",
_ => "something else",
}
}
fn classify_temperature(temp: f64) -> &'static str {
match temp as i32 {
i32::MIN..=-10 => "freezing",
-9..=0 => "very cold",
1..=15 => "cold",
16..=25 => "comfortable",
26..=35 => "warm",
_ => "hot",
}
}
fn main() {
for n in [-5, 1, 2, 5, 10, 42] {
println!("{}: {}", n, describe_number(n));
}
for temp in [-15.0, 5.0, 22.0, 38.0] {
println!("{:.0}°C is {}", temp, classify_temperature(temp));
}
}
if let and while let
The if let syntax provides a concise way to match a single pattern when you only care about one variant. It is syntactic sugar for a match expression with one arm and a wildcard. Similarly, while let continues looping as long as a pattern matches. These constructs reduce boilerplate when you do not need the exhaustiveness checking of a full match expression and only want to handle one specific case.
fn main() {
// if let for Option
let config_value: Option<i32> = Some(42);
if let Some(value) = config_value {
println!("Configuration value: {}", value);
} else {
println!("Using default configuration");
}
// while let with a stack
let mut stack = vec![1, 2, 3, 4, 5];
println!("Popping from stack:");
while let Some(top) = stack.pop() {
println!(" Got: {}", top);
}
println!("Stack is now empty");
// if let with enums
enum Command {
Quit,
Echo(String),
Move { x: i32, y: i32 },
}
let cmd = Command::Move { x: 10, y: 20 };
if let Command::Move { x, y } = cmd {
println!("Moving to ({}, {})", x, y);
}
}
Destructuring
Destructuring lets you break apart structs, enums, tuples, and references into their component parts within a pattern. You can destructure in let statements, function parameters, match arms, and for loops. Nested destructuring allows you to reach deep into complex data structures in a single pattern. The underscore pattern ignores a value, and the double-dot pattern ignores remaining fields.
struct Point {
x: f64,
y: f64,
}
fn distance_from_origin(Point { x, y }: &Point) -> f64 {
(x * x + y * y).sqrt()
}
fn main() {
// Tuple destructuring
let (first, second, third) = (1, "hello", 3.14);
println!("{}, {}, {}", first, second, third);
// Struct destructuring
let point = Point { x: 3.0, y: 4.0 };
let Point { x, y } = &point;
println!("Point({}, {}), distance = {:.2}", x, y,
distance_from_origin(&point));
// Nested destructuring
let ((a, b), Point { x: px, y: py }) = ((1, 2), Point { x: 5.0, y: 6.0 });
println!("a={}, b={}, px={}, py={}", a, b, px, py);
// Destructuring in a loop
let pairs = vec![(1, 'a'), (2, 'b'), (3, 'c')];
for (num, letter) in &pairs {
println!("{} -> {}", num, letter);
}
// Ignoring values with _
let (_, middle, _) = (10, 20, 30);
println!("Middle: {}", middle);
}
Advanced Pattern Matching
Rust patterns support match guards, binding with the at operator, and multiple patterns in a single arm. Match guards add an extra condition to a pattern arm using the if keyword. The at operator lets you bind a value to a variable while also testing it against a pattern. These features make Rust's pattern matching expressive enough to replace complex chains of conditional logic with clear, declarative code.
enum Message {
Hello { id: i32 },
Goodbye,
Data(Vec<i32>),
}
fn process_message(msg: &Message) {
match msg {
// Match guard
Message::Hello { id } if *id > 0 && *id < 100 => {
println!("Hello from valid id: {}", id);
}
// @ binding with range
Message::Hello { id: id_val @ 100..=999 } => {
println!("Hello from extended id: {}", id_val);
}
Message::Hello { id } => {
println!("Hello from other id: {}", id);
}
Message::Goodbye => println!("Goodbye!"),
// Match guard on data contents
Message::Data(v) if v.is_empty() => {
println!("Empty data message");
}
Message::Data(v) => {
println!("Data with {} elements, first: {}", v.len(), v[0]);
}
}
}
fn main() {
let messages = vec![
Message::Hello { id: 42 },
Message::Hello { id: 500 },
Message::Hello { id: -1 },
Message::Goodbye,
Message::Data(vec![]),
Message::Data(vec![10, 20, 30]),
];
for msg in &messages {
process_message(msg);
}
}