Rust - Match Expressions
Overview
Estimated time: 55–75 minutes
Master Rust's powerful match
expressions - one of Rust's most distinctive features. Learn pattern matching, destructuring, guards, and how match ensures exhaustive handling of all possible cases.
Learning Objectives
- Use match expressions for control flow and pattern matching
- Understand exhaustive matching and the catch-all pattern
- Destructure complex data types in match arms
- Use match guards for conditional patterns
- Apply match expressions to real-world scenarios
Prerequisites
Basic Match Syntax
Simple Match Expression
fn main() {
let number = 3;
let description = match number {
1 => "one",
2 => "two",
3 => "three",
4 => "four",
5 => "five",
_ => "something else", // Catch-all pattern
};
println!("The number {} is {}", number, description);
// Match can be used in different contexts
match number {
1..=5 => println!("Small number"),
6..=10 => println!("Medium number"),
_ => println!("Large number"),
}
}
Expected output:
The number 3 is three
Small number
Pattern Matching with Enums
Basic Enum Matching
#[derive(Debug)]
enum Direction {
Up,
Down,
Left,
Right,
}
fn move_player(direction: Direction) {
match direction {
Direction::Up => println!("Moving up"),
Direction::Down => println!("Moving down"),
Direction::Left => println!("Moving left"),
Direction::Right => println!("Moving right"),
}
}
fn main() {
let dir = Direction::Up;
move_player(dir);
let directions = vec![Direction::Left, Direction::Right, Direction::Down];
for d in directions {
move_player(d);
}
}
Expected output:
Moving up
Moving left
Moving right
Moving down
Matching Option and Result
fn safe_divide(a: f64, b: f64) -> Option<f64> {
if b == 0.0 {
None
} else {
Some(a / b)
}
}
fn main() {
let results = vec![
safe_divide(10.0, 2.0),
safe_divide(8.0, 0.0),
safe_divide(15.0, 3.0),
];
for result in results {
match result {
Some(value) => println!("Result: {:.2}", value),
None => println!("Cannot divide by zero!"),
}
}
// Parsing strings
let inputs = vec!["42", "abc", "123"];
for input in inputs {
let parse_result: Result<i32, _> = input.parse();
match parse_result {
Ok(number) => println!("Parsed '{}': {}", input, number),
Err(_) => println!("Failed to parse '{}'", input),
}
}
}
Expected output:
Result: 5.00
Cannot divide by zero!
Result: 5.00
Parsed '42': 42
Failed to parse 'abc'
Parsed '123': 123
Destructuring in Match
Struct Destructuring
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
fn describe_point(point: Point) {
match point {
Point { x: 0, y: 0 } => println!("Origin point"),
Point { x: 0, y } => println!("On Y-axis at y = {}", y),
Point { x, y: 0 } => println!("On X-axis at x = {}", x),
Point { x, y } if x == y => println!("Diagonal point at ({}, {})", x, y),
Point { x, y } => println!("Regular point at ({}, {})", x, y),
}
}
fn main() {
let points = vec![
Point { x: 0, y: 0 },
Point { x: 0, y: 5 },
Point { x: 3, y: 0 },
Point { x: 4, y: 4 },
Point { x: 2, y: 7 },
];
for point in points {
describe_point(point);
}
}
Expected output:
Origin point
On Y-axis at y = 5
On X-axis at x = 3
Diagonal point at (4, 4)
Regular point at (2, 7)
Match Guards and Advanced Patterns
Using Match Guards
fn categorize_number(n: i32) {
match n {
x if x < 0 => println!("{} is negative", x),
0 => println!("Zero"),
x if x % 2 == 0 => println!("{} is positive and even", x),
x if x % 2 == 1 => println!("{} is positive and odd", x),
_ => unreachable!(), // This should never be reached
}
}
fn main() {
let numbers = vec![-5, 0, 2, 7, 10];
for num in numbers {
categorize_number(num);
}
}
Expected output:
-5 is negative
Zero
2 is positive and even
7 is positive and odd
10 is positive and even
Practical Example: Simple Calculator
Expression Evaluator
#[derive(Debug, Clone)]
enum Expression {
Number(f64),
Add(Box<Expression>, Box<Expression>),
Subtract(Box<Expression>, Box<Expression>),
Multiply(Box<Expression>, Box<Expression>),
Divide(Box<Expression>, Box<Expression>),
}
fn evaluate(expr: &Expression) -> Result<f64, String> {
match expr {
Expression::Number(n) => Ok(*n),
Expression::Add(left, right) => {
let l = evaluate(left)?;
let r = evaluate(right)?;
Ok(l + r)
},
Expression::Subtract(left, right) => {
let l = evaluate(left)?;
let r = evaluate(right)?;
Ok(l - r)
},
Expression::Multiply(left, right) => {
let l = evaluate(left)?;
let r = evaluate(right)?;
Ok(l * r)
},
Expression::Divide(left, right) => {
let l = evaluate(left)?;
let r = evaluate(right)?;
if r == 0.0 {
Err("Division by zero".to_string())
} else {
Ok(l / r)
}
},
}
}
fn main() {
// Create expression: (10 + 5) * 2 - 8 / 4
let expr = Expression::Subtract(
Box::new(Expression::Multiply(
Box::new(Expression::Add(
Box::new(Expression::Number(10.0)),
Box::new(Expression::Number(5.0))
)),
Box::new(Expression::Number(2.0))
)),
Box::new(Expression::Divide(
Box::new(Expression::Number(8.0)),
Box::new(Expression::Number(4.0))
))
);
match evaluate(&expr) {
Ok(result) => println!("Result: {}", result),
Err(error) => println!("Error: {}", error),
}
}
Expected output:
Result: 28
Match vs If Let
When to Use Each Approach
fn main() {
let maybe_number = Some(42);
// Use match for exhaustive handling
let doubled = match maybe_number {
Some(n) => n * 2,
None => 0,
};
println!("Doubled: {}", doubled);
// Use if let for single pattern matching
if let Some(n) = maybe_number {
println!("The number is: {}", n);
} else {
println!("No number found");
}
// Match is better for multiple related patterns
let status_code = 404;
let message = match status_code {
200 => "OK",
404 => "Not Found",
500 => "Internal Server Error",
_ => "Unknown Status",
};
println!("Status: {}", message);
}
Expected output:
Doubled: 84
The number is: 42
Status: Not Found
Checks for Understanding
Question 1: Exhaustiveness
Why does Rust require match expressions to be exhaustive?
Show Answer
Answer: Exhaustive matching ensures that all possible cases are handled, preventing runtime errors from unhandled cases. This is part of Rust's safety guarantees - the compiler ensures you've considered all possibilities, making your code more reliable and preventing common bugs.
Question 2: Match Guards
What's the purpose of match guards and when would you use them?
Show Answer
Answer: Match guards (the if
condition after a pattern) allow you to add additional conditions to pattern matching. They're useful when you need to match on a pattern but also check additional conditions, like value ranges or comparisons with other variables.
Question 3: Pattern Binding
In the pattern Some(x) if x > 5
, what happens to the variable x
?
Show Answer
Answer: The variable x
is bound to the value inside the Some
variant and can be used both in the guard condition and in the match arm body. If the guard condition fails, the pattern doesn't match and x
is not accessible.