Rust - Advanced Patterns
Overview
Estimated time: 40–50 minutes
Master advanced pattern matching techniques in Rust, including match guards, range patterns, destructuring complex data structures, @ bindings, and pattern refutability. Build on basic pattern matching to handle sophisticated matching scenarios.
Learning Objectives
- Use match guards for conditional pattern matching.
- Apply range patterns and multiple pattern matching.
- Destructure nested structures and complex data types.
- Understand @ bindings and pattern binding.
- Distinguish between refutable and irrefutable patterns.
Prerequisites
Match Guards
Conditional Pattern Matching
Match guards allow you to add conditions to patterns using the if
keyword:
fn classify_number(x: i32) -> &'static str {
match x {
n if n < 0 => "negative",
n if n == 0 => "zero",
n if n > 0 && n <= 10 => "small positive",
n if n > 10 && n <= 100 => "medium positive",
_ => "large positive",
}
}
fn main() {
println!("{}", classify_number(-5)); // negative
println!("{}", classify_number(0)); // zero
println!("{}", classify_number(7)); // small positive
println!("{}", classify_number(50)); // medium positive
println!("{}", classify_number(200)); // large positive
}
Guards with Destructuring
Combine guards with pattern destructuring:
struct Person {
name: String,
age: u32,
city: String,
}
fn categorize_person(person: &Person) -> &'static str {
match person {
Person { age, city, .. } if *age < 18 => "minor",
Person { age, city, .. } if *age >= 18 && city == "New York" => "adult New Yorker",
Person { age, city, .. } if *age >= 65 => "senior citizen",
Person { city, .. } if city == "San Francisco" => "San Francisco resident",
_ => "regular adult",
}
}
fn main() {
let person1 = Person {
name: String::from("Alice"),
age: 16,
city: String::from("Boston"),
};
let person2 = Person {
name: String::from("Bob"),
age: 30,
city: String::from("New York"),
};
println!("{}", categorize_person(&person1)); // minor
println!("{}", categorize_person(&person2)); // adult New Yorker
}
Range Patterns
Numeric Ranges
Use range patterns to match values within a range:
fn classify_grade(score: u32) -> &'static str {
match score {
90..=100 => "A",
80..=89 => "B",
70..=79 => "C",
60..=69 => "D",
0..=59 => "F",
_ => "Invalid score",
}
}
fn main() {
println!("{}", classify_grade(95)); // A
println!("{}", classify_grade(73)); // C
println!("{}", classify_grade(45)); // F
}
Character Ranges
Range patterns work with characters too:
fn classify_char(c: char) -> &'static str {
match c {
'a'..='z' => "lowercase letter",
'A'..='Z' => "uppercase letter",
'0'..='9' => "digit",
' ' | '\t' | '\n' => "whitespace",
_ => "other character",
}
}
fn main() {
println!("{}", classify_char('m')); // lowercase letter
println!("{}", classify_char('5')); // digit
println!("{}", classify_char('@')); // other character
}
Complex Destructuring
Nested Structure Destructuring
Destructure deeply nested structures:
struct Address {
street: String,
city: String,
country: String,
}
struct Company {
name: String,
address: Address,
}
struct Employee {
name: String,
company: Company,
salary: Option,
}
fn describe_employee(employee: &Employee) -> String {
match employee {
Employee {
name,
company: Company {
name: company_name,
address: Address { city, country, .. }
},
salary: Some(sal),
} if *sal > 100000 => {
format!("{} is a high earner at {} in {}, {}", name, company_name, city, country)
}
Employee {
name,
company: Company { name: company_name, .. },
salary: Some(sal),
} => {
format!("{} works at {} earning ${}", name, company_name, sal)
}
Employee {
name,
company: Company { name: company_name, .. },
salary: None,
} => {
format!("{} works at {} (salary unknown)", name, company_name)
}
}
}
fn main() {
let employee = Employee {
name: String::from("Alice"),
company: Company {
name: String::from("TechCorp"),
address: Address {
street: String::from("123 Tech St"),
city: String::from("San Francisco"),
country: String::from("USA"),
},
},
salary: Some(120000),
};
println!("{}", describe_employee(&employee));
// Alice is a high earner at TechCorp in San Francisco, USA
}
@ Bindings
Binding While Matching
Use @ to bind values while pattern matching:
enum Message {
Hello { id: i32, name: String },
Quit,
Move { x: i32, y: i32 },
}
fn process_message(msg: Message) {
match msg {
Message::Hello { id: id_var @ 1..=5, name } => {
println!("Hello {} with low ID: {}", name, id_var);
}
Message::Hello { id: id_var @ 6..=10, name } => {
println!("Hello {} with medium ID: {}", name, id_var);
}
Message::Hello { id, name } => {
println!("Hello {} with ID: {}", name, id);
}
Message::Move { x: x_pos @ 0..=10, y: y_pos @ 0..=10 } => {
println!("Moving to small coordinates: ({}, {})", x_pos, y_pos);
}
Message::Move { x, y } => {
println!("Moving to: ({}, {})", x, y);
}
Message::Quit => println!("Quit message received"),
}
}
fn main() {
process_message(Message::Hello { id: 3, name: String::from("Alice") });
// Hello Alice with low ID: 3
process_message(Message::Move { x: 5, y: 8 });
// Moving to small coordinates: (5, 8)
}
Multiple Patterns
OR Patterns
Match multiple patterns using the |
operator:
fn day_type(day: &str) -> &'static str {
match day {
"Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" => "weekday",
"Saturday" | "Sunday" => "weekend",
_ => "unknown day",
}
}
fn is_vowel(c: char) -> bool {
match c.to_ascii_lowercase() {
'a' | 'e' | 'i' | 'o' | 'u' => true,
_ => false,
}
}
fn main() {
println!("{}", day_type("Wednesday")); // weekday
println!("{}", day_type("Saturday")); // weekend
println!("{}", is_vowel('A')); // true
println!("{}", is_vowel('B')); // false
}
Pattern Matching in Different Contexts
if let with Guards
Use complex patterns in if let
expressions:
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
if let Some(first @ 1..=3) = numbers.first() {
println!("First number is small: {}", first);
}
let point = (3, 4);
if let (x, y) if x * x + y * y == 25 = point {
println!("Point ({}, {}) is on circle with radius 5", x, y);
}
}
while let with Patterns
Use patterns in while let
loops:
fn main() {
let mut stack = vec![1, 2, 3, 4, 5];
while let Some(top @ 3..=5) = stack.pop() {
println!("Popped large number: {}", top);
}
println!("Remaining: {:?}", stack); // [1, 2]
}
Refutable vs Irrefutable Patterns
Understanding Pattern Types
Patterns can be refutable (can fail to match) or irrefutable (always match):
fn main() {
// Irrefutable patterns - always match
let x = 5; // Variable binding
let (a, b) = (1, 2); // Tuple destructuring
let Point { x, y } = point; // Struct destructuring (if point is Point)
// Refutable patterns - may not match
if let Some(value) = some_option { // Option may be None
println!("Got value: {}", value);
}
match some_result {
Ok(value) => println!("Success: {}", value),
Err(e) => println!("Error: {}", e),
}
}
Advanced Destructuring Examples
Array and Slice Patterns
Destructure arrays and slices with patterns:
fn analyze_slice(slice: &[i32]) {
match slice {
[] => println!("Empty slice"),
[x] => println!("Single element: {}", x),
[x, y] => println!("Two elements: {}, {}", x, y),
[first, .., last] => println!("First: {}, Last: {}", first, last),
[first, second, rest @ ..] if rest.len() > 2 => {
println!("First: {}, Second: {}, {} more elements", first, second, rest.len());
}
_ => println!("Other pattern"),
}
}
fn main() {
analyze_slice(&[]); // Empty slice
analyze_slice(&[1]); // Single element: 1
analyze_slice(&[1, 2]); // Two elements: 1, 2
analyze_slice(&[1, 2, 3]); // First: 1, Last: 3
analyze_slice(&[1, 2, 3, 4, 5, 6]); // First: 1, Second: 2, 4 more elements
}
Common Pitfalls
Mistakes to Avoid
- Unreachable patterns: More specific patterns must come before general ones
- Complex guard conditions: Keep guards simple and readable
- Overusing @ bindings: Use only when you need both the value and the pattern
- Missing exhaustiveness: Ensure all possible patterns are covered
Checks for Understanding
- What's the difference between
x @ 1..=10
and just1..=10
? - When should you use match guards instead of separate if statements?
- What makes a pattern refutable vs irrefutable?
- How do you match the first and last elements of a slice while ignoring the middle?
Answers
x @ 1..=10
binds the matched value tox
so you can use it;1..=10
just matches without binding- Use match guards when the condition depends on the matched values or when you want to keep related patterns together
- Refutable patterns can fail (like
Some(x)
); irrefutable patterns always succeed (like variable bindings) - Use
[first, .., last]
to match first and last elements while ignoring middle elements