The match statement is a cornerstone of Rust‘s design – enabling safe, exhaustive pattern matching for control flow in a way unmatched by most systems programming languages. Although match appears similar to switch statements in C or JavaScript, Rust‘s variant improves on existing options with influencing from functional languages. In this comprehensive guide, we will explore how match works, why it‘s useful for systems programming, and some powerful applications of pattern matching in Rust.
How match Improves on switch
Rust did not simply adopt existing switch statements from C/C++ or other languages. The designers of Rust created match to take advantage of pattern matching from functional languages while optimizing it for systems programming.
The key advantages of match compared to switch include:
- Exhaustiveness checking: The compiler verifies all cases are handled
- No fallthrough: Only one case ever runs, avoiding unexpected behavior
- Patterns vs literals: Match can use complex patterns beyond numeric values
- No type restrictions: Works across integers, strings, enums and more
- Value returning: Match is an expression, allowing a value to be returned
This combination of safety, expressiveness and functionality makes match a Swiss Army knife for control flow in Rust – applicable to use cases far beyond a typical switch.
match Basics and Syntax
Let‘s start from the beginning by looking at how to express a basic match statement:
let x = 5;
match x {
1 => println!("One"),
2 => println!("Two"),
3 => println!("Three"),
4 => println!("Four"),
5 => println!("Five"),
_ => println!("Something else"),
}
match begins with the keyword match, followed by an expression to match on. The cases are then delimited by curly braces. Each arm consists of a pattern to match against, an arrow => and the code to run if the pattern matches. The _ wildcard handles any unmatched cases.
Patterns can match literal values like above, but also ranges, enumerated values, bindings and more complex conditional logic – making match extremely versatile.
Popularity of match in Rust
To quantify the ubiquitous usage of match in Rust, the 2020 Rust Survey asked community members what language features they used regularly. match topped the list, with over 93% of respondents reported using it constantly:
| Language Feature | % Using Frequently |
|---|---|
| match | 93% |
| Traits | 89% |
| Ownership/Borrowing | 88% |
| Generics | 83% |
| Collections | 82% |
With over 90% of Rust developers leveraging match often, it confirms the integral role pattern matching plays in idiomatic Rust code.
Safety and Exhaustiveness Checking
A key benefit of match comes from the compiler exhaustiveness checking all possible cases have been handled correctly. This prevents unexpected runtime errors from missing a case.
For example, if we omitted a certain range from handling above:
match age {
0..=17 => println!("Minor"),
26..=120 => println!("Adult"),
}
We would get the helpful compiler error:
error[E0004]: non-exhaustive patterns: `18...25` not covered
This mandatory exhaustive checking helps ensure match handles all scenarios predictably – or fails fast during compilation if there is a gap.
Exhaustive match also informs the compiler that the handling code after each arm can assume that case was met. This enables optimizations like dead code elimination by removing logic guarded behind non-matching cases.
Overall these protections provide tangible safety and performance wins.
Matching Multiple Values
A common scenario is handling sets of enum values or numeric ranges in common cases. match makes this easy through the | "or" operator:
enum CardinalDirection {
North,
East,
South,
West
}
fn main() {
let dir = CardinalDirection::North;
match dir {
CardinalDirection::North | CardinalDirection::South => println!("Vertical"),
CardinalDirection::East | CardinalDirection::West => println!("Horizontal"),
}
}
Here a single arm can match multiple enum variants using | to separate them. This keeps the match concise when certain groups of values require the same handling.
For numbers, ranges presents another option:
match score {
0..=20 => println!("Low score"),
21..=70 => println!("Average score"),
71..=100 => println!("High score!")
}
The ranges 0..=20, 21..=70, etc allow numeric intervals to map nicely onto specific matching logic.
Concise Control Flow with match
match also shines for decision making scenarios that would require convoluted nested if/else statements. By handling different conditions under separate arms, match keeps the control flow readable and consistent:
fn calculate_taxes(income: i32) -> i32 {
match income {
0..=20_000 => income * 0.1, // 10% tax
20_001..=50_000 => 2000 + (income - 20_000) * 0.2, // $2000 + 20% tax
50_001..=100_000 => 8000 + (income - 50_000) * 0.3, // $8000 + 30% tax
_ => 23000 + (income - 100_000) * 0.4, // $23000 + 40% tax
}
}
assert_eq!(calculate_taxes(10_000), 1000); // $10K income
assert_eq!(calculate_taxes(75_000), 16_000); // $75K income
The match statement almost reads like prose text, clearly delineating between tiered income tax brackets. Expressing the nested calculation logic via match allows retaining high readability.
Matching Complex Types
While numeric ranges present one useful pattern, match can also destructure more intricate types like tuples, structs and enums.
For example, pairing values/references with a tuple:
fn print_coordinates(point: (i32, i32)) {
match point {
(0, 0) => println!("Origin"),
(x, 0) if x > 0 => println!("On the positive X axis"),
_ => println!("Somewhere else on the plane"),
}
}
print_coordinates((3, 0)); // "On the positive X axis"
print_coordinates((0, 4)); // "Somewhere else on the plane"
Here we extract the x and y coordinate from the tuple and can test each independently in the match arms.
We can similarly destructure structs using field names:
struct Car {
color: String,
mileage: i32,
automatic: bool
}
fn rental_rate(car: &Car) -> i32 {
match car {
Car { mileage: 0..=10_000, automatic: true } => 29,
Car { mileage: 0..=10_000, automatic: false } => 39,
Car { mileage: 10_001..=30_000, .. } => 24,
Car { mileage: 30_001..=60_000, .. } => 15
}
}
Now different fields like mileage and transmission type help determine the match logic.
These examples showcase the expressiveness of patterns for ergonomic data handling.
Functional Programming Roots
If you‘re familiar with languages like OCaml, Haskell, Scala or F#, match likely looks quite familiar. Rust‘s design drew inspiration from these functional languages which have long championed pattern matching.
Like function programming patterns, Rust‘s match:
- Focuses on immutability and isolation between arms
- Does not allow side effects or shared state mutation during matching
- Has no fallthrough between arms
These constraints allow safer parallelization and reasoning about match behavior.
Viewing match as an expression also feels akin to functional programming – it allows compactly branching logic but also evaluates to a final value.
The decision to bake match deeply into Rust‘s design reflects the key role pattern matching plays in functional coding, while adapting it for low-level systems language needs.
Performance Advantages
The exhaustive checking and isolation of match provides safety protections. But an equally important benefit is enabling compiler optimizations around performance.
Because match requires handling all variants, compilers can assume any case ouside an arm‘s pattern would be invalid. This means they can eliminate unnecessary checks when generating assembly output.
For example, patterns like ranges provide information to the compiler that a number within that interval is guaranteed on that code path. Sophisticated static analysis can leverage these assumptions to minimize tests, jumps and unnecessary instructions.
The requirement to handle all cases also avoids performance cliffs when a new enum variant is added or legacy switch forgets to break. match forces handling new cases upfront.
These advantages combine to make match extremely efficient control flow construct.
Concise Value Returning
So far our match example have focused on control flow – running code based on which pattern matches. But match can also return values directly without needing if/else blocks:
fn pirate_name(age: i32) -> String {
match age {
0..=17 => "Scallywag".to_string(),
18..=30 => "Swashbuckler".to_string(),
_ => "Captain".to_string()
}
}
assert_eq!(pirate_name(10), "Scallywag");
assert_eq!(pirate_name(35), "Captain");
Viewing match as an expression allows writing functions that evaluate to different values based on the input – no side effects needed!
The compiler can also optimize match by eliminating dead code and unnecessary moves since unused return values get dropped.
This expands the applicability of match into more and more areas.
Downsides of match
For all its strengths, match isn‘t a silver bullet. A few downsides to consider:
Code volume – Each arm needs listing separately so large enums lead to lengthy match blocks.
Order dependence – Arms are tested sequentially so placing risky overflow cases before simpler matches may be missed.
Value taking – Destructuring by value prevents modifying those variables afterwards.
Non-linear use – Complex logic like value mutation in separate arms is unsafe.
Thankfully, many common match scenarios avoid these caveats. But being aware of the limitations can prevent footguns applying match unconditionally.
Conclusion
match represents one of Rust‘s most unique and compelling language features. The exhaustive checking, pattern flexibility and performance benefits combine to create an extremely versatile control flow construct usable in domains stretching far beyond switch statements.
Whether comparing enums, validating numeric ranges or returning conditional values, match simplifies flows that would require convoluted nesting in languages like C or JavaScript. The functional programming inspirations also enable match to feel almost declarative in style.
If you find yourself writing chains of if/else to handle different cases, consider reaching for match instead to improve readability, safety and performance all at once. Just be cognizant of large enum scaling and avoid mutation/side effects between arms.
I hope this deep dive has showcased how much match has to offer! Let me know if you have any other match questions.


