Patterns and Matching in Rust
Patterns are a specialized syntax in Rust used to match against the structure of types, combining literals, destructured arrays, enums, structs, tuples, variables, wildcards, and placeholders. They describe the shape of data, allowing programs to verify data validity and control execution flow based on structural decomposition.
1. What Are Patterns and Where Are They Used?
A pattern consists of structural components that describe the shape of data. Rust uses patterns across multiple constructs to inspect, validate, and destructure data types.
Patterns are used in several core Rust language constructs:
matchexpressionsif letconditional expressionswhile letconditional loopsforloopsletstatements- Function and closure parameters
2. Match Expressions and Exhaustiveness
As introduced in Chapter 6, match expressions use patterns within their arms to branch program execution based on enum variants or data values.
Example: Enum Matching
enum Language {
English,
Spanish,
Russian,
Japanese,
}
fn main() {
let language = Language::English;
match language {
Language::English => println!("Hello!"),
Language::Spanish => println!("Hola!"),
Language::Russian => println!("Privet!"),
Language::Japanese => println!("Konnichiwa!"),
}
}
Note (Exhaustiveness): Rust requires
matchexpressions to be exhaustive. Every possible value of the matched type must be accounted for by the patterns. Omitting a variant results in a compile-time error.
Catch-All Patterns and Variable Binding
When you cannot list every single case or want a default fallback, you can use the underscore (_) catch-all pattern.
match language {
Language::English => println!("English"),
_ => println!("Unsupported language"),
}
If you need to use the unmatched value inside the catch-all arm, replace the underscore with a variable name (e.g., lang) to bind the value:
match language {
Language::English => println!("English"),
lang => println!("Unsupported language: {:?}", lang),
}
3. Conditional if let Expressions
When you only care about a single matching case and want to ignore all others without writing a verbose match with a catch-all, use if let.
Example: Multiple Conditional Checks with if let and else if let
fn main() {
let authorization_status: Option<&str> = None;
let is_admin = false;
let group_id: Result<u32, _> = "34".parse();
if let Some(status) = authorization_status {
println!("Authorization status: {}", status);
} else if is_admin {
println!("Authorization status: Admin");
} else if let Ok(group_id) = group_id {
if group_id > 30 {
println!("Authorization status: Privileged");
} else {
println!("Authorization status: Basic");
}
} else {
println!("Authorization status: Guest");
}
}
Note (Variable Shadowing): In the
let Ok(group_id) = group_idarm, the inner pattern variablegroup_idshadows the outergroup_idvariable for the scope of that block.Limitation: Unlike
match,if letexpressions are not enforced to be exhaustive by the compiler. Omitting anelsebranch will compile successfully, which can lead to unhandled edge cases.
4. Conditional Loops with while let
The while let conditional loop allows running a loop as long as a specific pattern continues to match successfully.
Example: Processing a Stack Vector
fn main() {
let mut stack = Vec::new();
stack.push(1);
stack.push(2);
stack.push(3);
// Pop returns Option<T>. Loop runs while Some(val) is returned.
while let Some(top) = stack.pop() {
println!("{}", top);
}
}
Execution Flow:
5. Iteration and Destructuring in for Loops
In a for loop, the pattern is the construct directly following the for keyword. Methods like .enumerate() return tuples (index, value), which can be destructured directly in the loop header.
Example: Enumerating Vectors
fn main() {
let v = vec!['a', 'b', 'c'];
for (index, value) in v.iter().enumerate() {
println!("Index: {}, Value: {}", index, value);
}
}
6. Patterns in let Statements
Many developers view let statements as simple variable assignments, but they are formally pattern matching statements.
Formal Syntax Structure
let PATTERN = EXPRESSION;
When you write let x = 5;, x is an irrefutable pattern that matches any value and binds 5 to it.
Tuple Destructuring in let
fn main() {
let (x, y, z) = (1, 2, 3);
println!("x: {}, y: {}, z: {}", x, y, z);
}
If the pattern shape does not match the expression shape, a compile-time error occurs:
// Compile-time error: expected tuple pattern with 3 elements, found 2
let (x, y) = (1, 2, 3);
Ignoring Values with Underscores
To ignore specific positions within a destructured expression, use the underscore (_) placeholder:
fn main() {
let (x, y, _) = (1, 2, 3); // Ignores the third element
}
7. Function and Closure Parameters as Patterns
Function and closure parameters are also patterns, allowing you to destructure complex arguments directly in the function signature.
Example: Destructuring a Tuple Parameter
fn print_coordinates(&(x, y): &(i32, i32)) {
println!("Current location: ({}, {})", x, y);
}
fn main() {
let point = (3, 5);
print_coordinates(&point);
}
8. Refutability: Refutable vs. Irrefutable Patterns
Patterns are categorized into two types based on whether they can fail to match for any possible value passed to them:
- Irrefutable Patterns: Patterns that will never fail to match any value provided.
- Examples:
letstatements, function parameters,forloop variables.
- Examples:
- Refutable Patterns: Patterns that can fail to match for certain values.
- Examples:
if let,while let,matcharms.
- Examples:
Summary Table of Pattern Contexts
Compilation Errors and Warnings Examples
1. Using a refutable pattern in a let statement (Compile Error):
let x: Option<i32> = None;
let Some(y) = x; // Error: let bindings require an irrefutable pattern
2. Using an irrefutable pattern in an if let (Compiler Warning):
let x = 5;
if let x = 5 { // Warning: this pattern will always match, if let is useless
println!("x is {}", x);
}
Summary and Key Takeaways
- Definition: Patterns describe the structural shape of data, combining literals, enums, structs, tuples, variables, and wildcards.
- Exhaustiveness:
matchexpressions must be exhaustive; every possible value of the data type must be handled. - Catch-Alls: Use
_as a wildcard or name a variable in the catch-all arm to bind the unmatched value. if let&while let: Provide concise syntax for matching single variants without full exhaustive match arms, though they do not enforce exhaustiveness.letStatements as Patterns:letstatements are formal pattern-matching assignments (let PATTERN = EXPRESSION;).- Function Parameters: Function and closure parameters support pattern destructuring directly in their signatures.
- Refutability: Irrefutable patterns always match (used in
let,for, parameters); refutable patterns can fail to match (used inmatch,if let,while let). - Compiler Safeguards: Rust generates compile-time errors for non-exhaustive matches or refutable
letbindings, and warnings for useless irrefutableif letchecks.