Pattern matching stands as one of Rust’s most powerful and distinctive features, elevating it beyond a mere control flow mechanism to a fundamental aspect of the language’s design philosophy. Unlike the simple switch statements found in many languages, Rust’s pattern matching system provides a rich, expressive way to destructure complex data types, handle multiple conditions, and ensure exhaustive checking of all possible cases. This combination of power and safety makes pattern matching an essential tool in every Rust programmer’s toolkit.
In this comprehensive guide, we’ll explore the full spectrum of Rust’s pattern matching capabilities, from basic match expressions to advanced destructuring patterns. You’ll learn how to leverage pattern matching to write code that is not only more concise and readable but also more robust and error-resistant. By the end, you’ll understand why pattern matching is considered one of Rust’s crown jewels and how to use it effectively in your own projects.
The Basics of Pattern Matching
At its core, pattern matching in Rust is implemented through the match
expression, which compares a value against a series of patterns and executes code based on which pattern matches:
fn main() {
let number = 13;
match number {
// Match a single value
0 => println!("Zero"),
// Match multiple values
1 | 2 => println!("One or Two"),
// Match a range
3..=9 => println!("Between Three and Nine"),
// Catch-all pattern
_ => println!("Something else: {}", number),
}
}
The match
expression evaluates to a value, making it an expression rather than just a statement:
fn main() {
let number = 13;
let description = match number {
0 => "Zero",
1 | 2 => "One or Two",
3..=9 => "Between Three and Nine",
_ => "Something else",
};
println!("{} is {}", number, description);
}
Exhaustiveness Checking
One of the most powerful aspects of Rust’s pattern matching is exhaustiveness checking. The compiler ensures that all possible cases are handled:
enum Direction {
North,
South,
East,
West,
}
fn describe_direction(direction: Direction) -> &'static str {
match direction {
Direction::North => "Heading north",
Direction::South => "Heading south",
Direction::East => "Heading east",
// Removing this line would cause a compile error
Direction::West => "Heading west",
}
}
If we were to add a new variant to the Direction
enum, the compiler would force us to update all match
expressions that use it, preventing bugs that would occur if we forgot to handle the new case.
Pattern Types
Rust supports a wide variety of pattern types, each serving different purposes:
Literal Patterns
Match against specific literal values:
fn main() {
let x = 1;
match x {
1 => println!("One"),
2 => println!("Two"),
_ => println!("Something else"),
}
}
Variable Patterns
Bind matched values to new variables:
fn main() {
let x = 5;
match x {
n => println!("Matched {}", n),
}
}
Multiple Patterns
Match against multiple values using the |
operator:
fn main() {
let x = 1;
match x {
1 | 2 | 3 => println!("One, two, or three"),
_ => println!("Something else"),
}
}
Range Patterns
Match against a range of values:
fn main() {
let x = 5;
match x {
1..=5 => println!("One through five"),
6..=10 => println!("Six through ten"),
_ => println!("Something else"),
}
}
Wildcard Pattern
The _
pattern matches any value without binding it:
fn main() {
let x = 10;
match x {
1 => println!("One"),
2 => println!("Two"),
_ => println!("Something else"),
}
}
Reference Patterns
Match against references and dereference values:
fn main() {
let reference = &4;
match reference {
&val => println!("Got a value via reference: {}", val),
}
// Alternative approach with dereference patterns
match *reference {
val => println!("Got a value via dereferencing: {}", val),
}
}
Binding with @ Operator
Bind a value while also testing it against a pattern:
fn main() {
let x = 5;
match x {
n @ 1..=5 => println!("Matched {} in range 1-5", n),
n @ 6..=10 => println!("Matched {} in range 6-10", n),
_ => println!("Didn't match a range"),
}
}
Destructuring Complex Data Types
One of the most powerful aspects of pattern matching is the ability to destructure complex data types:
Destructuring Tuples
fn main() {
let point = (3, 5);
match point {
(0, 0) => println!("Origin"),
(0, y) => println!("X-axis at y={}", y),
(x, 0) => println!("Y-axis at x={}", x),
(x, y) => println!("Point at ({}, {})", x, y),
}
}
Destructuring Arrays
fn main() {
let array = [1, 2, 3];
match array {
[1, _, _] => println!("Array starts with 1"),
[_, 2, _] => println!("Array has 2 in the middle"),
[_, _, 3] => println!("Array ends with 3"),
_ => println!("No pattern matched"),
}
}
Destructuring Slices
fn main() {
let slice = &[1, 2, 3, 4, 5];
match slice {
[first, second, ..] => println!("First two elements: {}, {}", first, second),
[] => println!("Empty slice"),
}
match slice {
[first, .., last] => println!("First: {}, Last: {}", first, last),
[] => println!("Empty slice"),
}
match slice {
[first, middle @ .., last] => {
println!("First: {}, Middle: {:?}, Last: {}", first, middle, last);
}
[] => println!("Empty slice"),
}
}
Destructuring Structs
struct Point {
x: i32,
y: i32,
}
fn main() {
let point = Point { x: 0, y: 7 };
match point {
Point { x: 0, y } => println!("On the y-axis at {}", y),
Point { x, y: 0 } => println!("On the x-axis at {}", x),
Point { x, y } => println!("At coordinates ({}, {})", x, y),
}
// Shorthand when variable names match field names
match point {
Point { x: 0, y } => println!("On the y-axis at {}", y),
Point { x, y: 0 } => println!("On the x-axis at {}", x),
Point { x, y } => println!("At coordinates ({}, {})", x, y),
}
}
Destructuring Enums
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn main() {
let msg = Message::ChangeColor(0, 160, 255);
match msg {
Message::Quit => println!("Quit"),
Message::Move { x, y } => println!("Move to ({}, {})", x, y),
Message::Write(text) => println!("Text message: {}", text),
Message::ChangeColor(r, g, b) => {
println!("Change color to RGB({}, {}, {})", r, g, b);
}
}
}
Nested Destructuring
enum Color {
Rgb(i32, i32, i32),
Hsv(i32, i32, i32),
}
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(Color),
}
fn main() {
let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));
match msg {
Message::ChangeColor(Color::Rgb(r, g, b)) => {
println!("Change color to RGB({}, {}, {})", r, g, b);
}
Message::ChangeColor(Color::Hsv(h, s, v)) => {
println!("Change color to HSV({}, {}, {})", h, s, v);
}
_ => println!("Other message"),
}
}
Guards in Pattern Matching
Pattern guards allow for additional boolean conditions in match arms:
fn main() {
let num = 4;
match num {
n if n < 0 => println!("Negative number: {}", n),
n if n > 0 && n % 2 == 0 => println!("Positive even number: {}", n),
n if n > 0 => println!("Positive odd number: {}", n),
_ => println!("Zero"),
}
}
Guards can be combined with destructuring:
struct Point {
x: i32,
y: i32,
}
fn main() {
let point = Point { x: 4, y: -3 };
match point {
Point { x, y } if x > 0 && y > 0 => println!("First quadrant: ({}, {})", x, y),
Point { x, y } if x < 0 && y > 0 => println!("Second quadrant: ({}, {})", x, y),
Point { x, y } if x < 0 && y < 0 => println!("Third quadrant: ({}, {})", x, y),
Point { x, y } if x > 0 && y < 0 => println!("Fourth quadrant: ({}, {})", x, y),
Point { x: 0, y } => println!("On the y-axis at {}", y),
Point { x, y: 0 } => println!("On the x-axis at {}", x),
Point { x: 0, y: 0 } => println!("At the origin"),
_ => unreachable!(),
}
}
Pattern Matching Beyond match
While match
is the most obvious use of pattern matching, Rust incorporates patterns in several other contexts:
if let Expressions
The if let
syntax allows for concise matching when you only care about one pattern:
fn main() {
let some_value = Some(5);
// Using match
match some_value {
Some(value) => println!("Got a value: {}", value),
None => (),
}
// Using if let (more concise)
if let Some(value) = some_value {
println!("Got a value: {}", value);
}
}
if let
can be combined with else
:
fn main() {
let number = Some(7);
if let Some(n) = number {
println!("Number is {}", n);
} else {
println!("No number found");
}
}
while let Loops
Similar to if let
, while let
continues a loop as long as a pattern matches:
fn main() {
let mut stack = Vec::new();
stack.push(1);
stack.push(2);
stack.push(3);
// Pop values off the stack while it's not empty
while let Some(top) = stack.pop() {
println!("Popped: {}", top);
}
}
for Loops
The for
loop uses pattern matching to destructure items:
fn main() {
let v = vec![(1, 'a'), (2, 'b'), (3, 'c')];
for (number, letter) in v {
println!("{}: {}", number, letter);
}
}
let Statements
Every let
statement uses pattern matching:
fn main() {
// Simple binding
let x = 5;
// Destructuring a tuple
let (x, y, z) = (1, 2, 3);
// Destructuring a struct
let Point { x, y } = Point { x: 10, y: 20 };
println!("x: {}, y: {}, z: {}", x, y, z);
}
Function Parameters
Function parameters also use pattern matching:
fn print_coordinates(&(x, y): &(i32, i32)) {
println!("Current location: ({}, {})", x, y);
}
fn main() {
let point = (3, 5);
print_coordinates(&point);
}
Refutability: Matching Patterns That Might Fail
Patterns come in two forms:
- Irrefutable patterns: Always match (used in
let
statements, function parameters, andfor
loops) - Refutable patterns: Might not match (used in
match
expressions,if let
, andwhile let
)
fn main() {
// Irrefutable pattern (always matches)
let x = 5;
// Refutable pattern (might not match)
if let Some(value) = Some(5) {
println!("Got a value: {}", value);
}
// This would be a compile error because let requires an irrefutable pattern
// let Some(value) = Some(5);
// This works because we're handling all cases
let value = match Some(5) {
Some(val) => val,
None => 0,
};
}
Advanced Pattern Matching Techniques
Binding Parts of Patterns
The @
operator lets you bind a value while also testing it against a pattern:
fn main() {
let num = 5;
match num {
n @ 1..=5 => println!("Got a number in range 1-5: {}", n),
n @ 6..=10 => println!("Got a number in range 6-10: {}", n),
_ => println!("Number out of range"),
}
}
This is particularly useful when destructuring complex structures:
#[derive(Debug)]
enum Message {
Hello { id: i32 },
}
fn main() {
let msg = Message::Hello { id: 5 };
match msg {
Message::Hello { id: id_var @ 3..=7 } => {
println!("Found an id in range [3, 7]: {}", id_var);
}
Message::Hello { id: 10..=12 } => {
println!("Found an id in another range");
}
Message::Hello { id } => {
println!("Found some other id: {}", id);
}
}
}
Multiple Patterns with Bindings
When using the |
operator with bindings, the variables must be bound in all patterns:
fn main() {
let x = 1;
match x {
// This works because 'n' is bound in both patterns
n @ 1 | n @ 2 => println!("Got 1 or 2: {}", n),
_ => println!("Something else"),
}
}
Ignoring Parts of Values
Rust provides several ways to ignore parts of values in patterns:
fn main() {
// Ignoring an entire value with _
let _ = 5;
// Ignoring parts of a tuple
let (_, y, _) = (1, 2, 3);
println!("y: {}", y);
// Ignoring parts of a struct
struct Point {
x: i32,
y: i32,
z: i32,
}
let point = Point { x: 0, y: 0, z: 0 };
let Point { x, .. } = point;
println!("x: {}", x);
// Ignoring parts of a nested structure
let numbers = (2, 4, 8, 16, 32);
let (first, .., last) = numbers;
println!("First: {}, Last: {}", first, last);
}
Match Guards with Multiple Patterns
Match guards apply to the entire pattern:
fn main() {
let x = 4;
let y = false;
match x {
4 | 5 | 6 if y => println!("Yes"),
_ => println!("No"),
}
}
In this example, the guard if y
applies to all patterns 4 | 5 | 6
.
Real-World Pattern Matching Examples
Parsing Command-Line Arguments
enum Command {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(u8, u8, u8),
}
fn parse_command(input: &str) -> Option<Command> {
let parts: Vec<&str> = input.trim().split_whitespace().collect();
match parts.as_slice() {
["quit"] => Some(Command::Quit),
["move", x, y] if x.parse::<i32>().is_ok() && y.parse::<i32>().is_ok() => {
Some(Command::Move {
x: x.parse().unwrap(),
y: y.parse().unwrap(),
})
}
["write", text @ ..] => {
Some(Command::Write(text.join(" ")))
}
["color", r, g, b]
if r.parse::<u8>().is_ok() &&
g.parse::<u8>().is_ok() &&
b.parse::<u8>().is_ok() => {
Some(Command::ChangeColor(
r.parse().unwrap(),
g.parse().unwrap(),
b.parse().unwrap(),
))
}
_ => None,
}
}
fn main() {
let commands = [
"quit",
"move 10 20",
"write Hello World",
"color 255 128 0",
"invalid command",
];
for cmd in commands {
match parse_command(cmd) {
Some(Command::Quit) => println!("Quitting"),
Some(Command::Move { x, y }) => println!("Moving to ({}, {})", x, y),
Some(Command::Write(text)) => println!("Writing: {}", text),
Some(Command::ChangeColor(r, g, b)) => {
println!("Changing color to RGB({}, {}, {})", r, g, b);
}
None => println!("Unknown command: {}", cmd),
}
}
}
State Machine Implementation
enum State {
Start,
Processing { steps_completed: u32 },
Finished { success: bool },
}
enum Event {
Begin,
Step { completed: bool },
Abort,
Complete { success: bool },
}
fn transition(state: State, event: Event) -> State {
match (state, event) {
(State::Start, Event::Begin) => {
State::Processing { steps_completed: 0 }
}
(State::Processing { steps_completed }, Event::Step { completed: true }) => {
State::Processing { steps_completed: steps_completed + 1 }
}
(State::Processing { steps_completed }, Event::Step { completed: false }) => {
State::Processing { steps_completed }
}
(State::Processing { .. }, Event::Complete { success }) => {
State::Finished { success }
}
(State::Processing { .. }, Event::Abort) => {
State::Finished { success: false }
}
(state, event) => {
println!("Invalid transition");
state
}
}
}
Error Handling with Pattern Matching
use std::fs::File;
use std::io::{self, Read};
enum ReadError {
FileNotFound(String),
PermissionDenied(String),
IoError(io::Error),
}
fn read_file(path: &str) -> Result<String, ReadError> {
let file = match File::open(path) {
Ok(file) => file,
Err(error) => match error.kind() {
io::ErrorKind::NotFound => {
return Err(ReadError::FileNotFound(path.to_string()))
}
io::ErrorKind::PermissionDenied => {
return Err(ReadError::PermissionDenied(path.to_string()))
}
_ => return Err(ReadError::IoError(error)),
},
};
let mut contents = String::new();
match file.read_to_string(&mut contents) {
Ok(_) => Ok(contents),
Err(error) => Err(ReadError::IoError(error)),
}
}
fn main() {
let result = read_file("example.txt");
match result {
Ok(contents) => println!("File contents: {}", contents),
Err(ReadError::FileNotFound(path)) => {
println!("File not found: {}", path);
}
Err(ReadError::PermissionDenied(path)) => {
println!("Permission denied: {}", path);
}
Err(ReadError::IoError(error)) => {
println!("I/O error: {}", error);
}
}
}
Best Practices for Pattern Matching
1. Leverage Exhaustiveness Checking
Let the compiler help you ensure all cases are handled:
enum Direction {
North,
South,
East,
West,
}
fn describe_direction(direction: Direction) -> &'static str {
// If we add a new variant to Direction, this will fail to compile
// until we handle the new case
match direction {
Direction::North => "Heading north",
Direction::South => "Heading south",
Direction::East => "Heading east",
Direction::West => "Heading west",
}
}
2. Use Specific Patterns Before General Ones
More specific patterns should come before more general ones:
fn describe_number(n: i32) -> &'static str {
match n {
0 => "Zero",
1 => "One",
_ if n < 0 => "Negative",
_ if n % 2 == 0 => "Even",
_ => "Odd",
}
}
3. Choose the Right Pattern Matching Construct
Use the most appropriate construct for your needs:
match
for exhaustive checkingif let
for single pattern caseswhile let
for conditional loopslet
destructuring for simple assignments
4. Use Nested Patterns for Complex Data
Break down complex structures with nested patterns:
enum Color {
Rgb(u8, u8, u8),
Hsv(u8, u8, u8),
}
enum Shape {
Circle { radius: f64, color: Color },
Rectangle { width: f64, height: f64, color: Color },
}
fn describe_shape(shape: Shape) -> String {
match shape {
Shape::Circle { radius, color: Color::Rgb(r, g, b) } => {
format!("RGB colored circle with radius {}", radius)
}
Shape::Circle { radius, color: Color::Hsv(h, s, v) } => {
format!("HSV colored circle with radius {}", radius)
}
Shape::Rectangle { width, height, color: Color::Rgb(r, g, b) } => {
format!("RGB colored rectangle {}x{}", width, height)
}
Shape::Rectangle { width, height, color: Color::Hsv(h, s, v) } => {
format!("HSV colored rectangle {}x{}", width, height)
}
}
}
5. Use Guards for Complex Conditions
When patterns alone aren’t enough, use guards for additional conditions:
fn classify_point(x: i32, y: i32) -> &'static str {
match (x, y) {
(0, 0) => "Origin",
(x, y) if x.abs() == y.abs() => "On diagonal",
(0, _) => "On y-axis",
(_, 0) => "On x-axis",
(x, y) if x > 0 && y > 0 => "First quadrant",
(x, y) if x < 0 && y > 0 => "Second quadrant",
(x, y) if x < 0 && y < 0 => "Third quadrant",
(x, y) if x > 0 && y < 0 => "Fourth quadrant",
_ => unreachable!(),
}
}
Conclusion
Pattern matching in Rust is far more than a simple control flow mechanism—it’s a powerful feature that permeates the entire language. From basic match
expressions to complex destructuring patterns, Rust’s pattern matching capabilities enable you to write code that is not only more concise and readable but also more robust and error-resistant.
The key strengths of Rust’s pattern matching system include:
- Exhaustiveness checking, which ensures all possible cases are handled
- Rich pattern types, from simple literals to complex destructuring patterns
- Integration throughout the language, from
match
expressions tolet
bindings - Guards for complex conditions, extending the power of pattern matching beyond simple structural matching
By mastering pattern matching, you gain access to one of Rust’s most distinctive and powerful features. You’ll write code that more clearly expresses your intent, handles edge cases more reliably, and leverages the compiler’s ability to catch errors at compile time rather than runtime.
As you continue your journey with Rust, look for opportunities to use pattern matching to simplify complex logic, handle variants of data structures, and make your code more expressive. The initial investment in learning these techniques pays dividends in code that is both more elegant and more robust—a winning combination for any serious software project.