Pattern Matching in Rust
Pattern matching is one of Rust's most powerful features. It lets you compare a value against a series of patterns and execute code based on which pattern matches. Unlike simple switch statements in other languages, Rust's pattern matching is exhaustive, meaning the compiler ensures you handle every possible case. Combined with destructuring, it becomes an expressive tool for writing concise and safe code.
The match Expression
The match expression compares a value against a series of patterns. Each pattern is called an arm, and Rust guarantees that all possible values are covered:
fn main() {
let number = 7;
match number {
1 => println!("One"),
2 => println!("Two"),
3 => println!("Three"),
4..=6 => println!("Between four and six"),
7 | 8 => println!("Seven or eight"),
9..=20 => println!("Between nine and twenty"),
_ => println!("Something else"),
}
}
The _ pattern is a wildcard that matches anything. It is commonly used as the last arm to handle all remaining cases. The | operator lets you combine multiple patterns in a single arm, and ..= creates inclusive ranges.
match Returns a Value
Since match is an expression in Rust, it produces a value. This means you can assign the result of a match to a variable:
fn main() {
let coin = "quarter";
let value_in_cents = match coin {
"penny" => 1,
"nickel" => 5,
"dime" => 10,
"quarter" => 25,
_ => {
println!("Unknown coin: {}", coin);
0
}
};
println!("A {} is worth {} cents", coin, value_in_cents);
}
Notice that each arm must return the same type. If an arm has multiple statements, wrap them in braces and ensure the last expression is the return value.
Matching Enums
Pattern matching truly shines when working with enums. Because match is exhaustive, the compiler forces you to handle every variant, which prevents bugs caused by forgotten cases:
enum HttpStatus {
Ok,
NotFound,
InternalError,
Redirect(String),
Custom(u16, String),
}
fn describe_status(status: HttpStatus) -> String {
match status {
HttpStatus::Ok => String::from("200 - Everything is fine"),
HttpStatus::NotFound => String::from("404 - Resource not found"),
HttpStatus::InternalError => String::from("500 - Server error"),
HttpStatus::Redirect(url) => format!("301 - Redirecting to {}", url),
HttpStatus::Custom(code, msg) => format!("{} - {}", code, msg),
}
}
fn main() {
let statuses = vec![
HttpStatus::Ok,
HttpStatus::NotFound,
HttpStatus::Redirect(String::from("https://example.com")),
HttpStatus::Custom(418, String::from("I'm a teapot")),
];
for status in statuses {
println!("{}", describe_status(status));
}
}
Destructuring in Match Arms
You can destructure structs, tuples, and nested types directly inside match arms to extract the data you need:
struct Point {
x: i32,
y: i32,
}
fn classify_point(point: &Point) -> &str {
match (point.x, point.y) {
(0, 0) => "at the origin",
(x, 0) if x > 0 => "on the positive x-axis",
(0, y) if y > 0 => "on the positive y-axis",
(x, y) if x > 0 && y > 0 => "in the first quadrant",
(x, y) if x < 0 && y > 0 => "in the second quadrant",
_ => "somewhere else",
}
}
fn main() {
let points = vec![
Point { x: 0, y: 0 },
Point { x: 5, y: 0 },
Point { x: 3, y: 7 },
Point { x: -2, y: 4 },
];
for p in &points {
println!("({}, {}) is {}", p.x, p.y, classify_point(p));
}
}
Match Guards
A match guard is an additional if condition on a match arm. The arm only matches if both the pattern and the guard are satisfied:
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
for &num in &numbers {
let description = match num {
n if n % 2 == 0 && n > 5 => "large even",
n if n % 2 == 0 => "small even",
n if n > 5 => "large odd",
_ => "small odd",
};
println!("{} is {}", num, description);
}
}
Guards are useful when you need to test conditions that cannot be expressed purely through patterns, like arithmetic comparisons or calling functions.
if let for Simple Patterns
When you only care about one pattern and want to ignore the rest, if let provides a cleaner syntax than a full match:
fn main() {
let config_value: Option<u32> = Some(3);
// With match - verbose for a single case
match config_value {
Some(val) => println!("Config value: {}", val),
None => {}
}
// With if let - much cleaner
if let Some(val) = config_value {
println!("Config value (if let): {}", val);
}
// if let with else
let missing: Option<u32> = None;
if let Some(val) = missing {
println!("Found: {}", val);
} else {
println!("No value configured, using default");
}
}
while let for Iterative Matching
The while let construct repeatedly matches a pattern in a loop. It keeps running as long as the pattern continues to match, which is especially useful for consuming iterators or popping from collections:
fn main() {
let mut stack = vec![1, 2, 3, 4, 5];
// Pop elements until the stack is empty
while let Some(top) = stack.pop() {
println!("Popped: {}", top);
}
println!("Stack is now empty: {:?}", stack);
// while let with an iterator
let names = vec!["Alice", "Bob", "Charlie"];
let mut iter = names.iter();
while let Some(name) = iter.next() {
println!("Hello, {}!", name);
}
}
Nested Pattern Matching
Patterns can be nested to match complex data structures. This is particularly powerful when working with enums that contain other enums or structs:
enum Command {
Move { x: i32, y: i32 },
Write(String),
Quit,
}
enum AppEvent {
UserCommand(Command),
SystemAlert(String),
Tick(u64),
}
fn handle_event(event: AppEvent) {
match event {
AppEvent::UserCommand(Command::Move { x, y }) => {
println!("User moved to ({}, {})", x, y);
}
AppEvent::UserCommand(Command::Write(text)) => {
println!("User wrote: {}", text);
}
AppEvent::UserCommand(Command::Quit) => {
println!("User quit the application");
}
AppEvent::SystemAlert(msg) => {
println!("ALERT: {}", msg);
}
AppEvent::Tick(ms) => {
println!("Tick at {} ms", ms);
}
}
}
fn main() {
let events = vec![
AppEvent::UserCommand(Command::Move { x: 10, y: 20 }),
AppEvent::SystemAlert(String::from("Low memory")),
AppEvent::UserCommand(Command::Write(String::from("Hello!"))),
AppEvent::Tick(1500),
AppEvent::UserCommand(Command::Quit),
];
for event in events {
handle_event(event);
}
}
Try It Yourself
Combine multiple pattern matching techniques in a single program. Try modifying the temperature thresholds or adding new weather conditions:
enum Temperature {
Celsius(f64),
Fahrenheit(f64),
}
fn to_celsius(temp: &Temperature) -> f64 {
match temp {
Temperature::Celsius(c) => *c,
Temperature::Fahrenheit(f) => (f - 32.0) * 5.0 / 9.0,
}
}
fn weather_advisory(temp: &Temperature) -> &str {
let celsius = to_celsius(temp);
match celsius as i32 {
c if c < -10 => "Extreme cold warning! Stay indoors.",
-10..=0 => "Freezing conditions. Bundle up!",
1..=15 => "Cool weather. Wear a jacket.",
16..=25 => "Pleasant weather. Enjoy your day!",
26..=35 => "Hot weather. Stay hydrated.",
_ => "Extreme heat warning! Seek shade.",
}
}
fn main() {
let readings = vec![
Temperature::Celsius(22.0),
Temperature::Fahrenheit(14.0),
Temperature::Celsius(-15.0),
Temperature::Fahrenheit(98.6),
];
for temp in &readings {
let celsius = to_celsius(temp);
let original = match temp {
Temperature::Celsius(c) => format!("{:.1} C", c),
Temperature::Fahrenheit(f) => format!("{:.1} F", f),
};
println!("{} ({:.1} C): {}", original, celsius, weather_advisory(temp));
}
}
Key Takeaways
matchis exhaustive -- the compiler ensures you handle all possible cases- Patterns can include literals, ranges (
..=), wildcards (_), and alternatives (|) - Match guards (
ifconditions) add extra flexibility to pattern arms - Destructuring extracts values from enums, structs, and tuples inside patterns
if letsimplifies code when you only care about one patternwhile letrepeats as long as a pattern matches, great for consuming iterators and stacks- Patterns can be nested to match deeply structured data
Pro Tip: If you find yourself writing a
matchwith many arms where most do the same thing, consider usingif letfor the one case you care about, or restructure your code with methods on the enum to encapsulate the matching logic.
Next Steps
Now that you can destructure data with patterns, we'll go deeper into borrowing — the rules that govern how references let different parts of your program share data safely without copying it.
Next lesson
Borrowing in Depth
Deep dive into Rust borrowing and references — learn shared and mutable references, the borrow checker, and safe data access
25 min