Rust · lesson 13 of 30
Beginner
15- 1 Introduction to Rust
- 2 Setting Up Rust
- 3 Variables in Rust
- 4 Data Types in Rust
- 5 Operators in Rust
- 6 Control Flow in Rust
- 7 Functions in Rust
- 8 Ownership in Rust
- 9 Borrowing and References
- 11 Structs in Rust
- 12 Enums in Rust
- 13 Pattern Matching in Rust
- 17 Collections in Rust
- 25 Testing in Rust
- 26 File I/O in Rust
Intermediate
9Advanced
6Pattern Matching in Rust
Master Rust pattern matching: match, ranges, destructuring tuples, structs and nested enums, guards, @ bindings, if let, while let, slice patterns.
By SkillByExample · Updated
match Expressions
match is one of Rust’s most powerful features. It compares a value against a series of patterns and executes the first matching arm — and crucially, it is an expression that produces a value. Unlike switch in C or Java, match in Rust is exhaustive: the compiler rejects any match that does not cover every possible value. This means you can never accidentally forget a case.
fn main() {
for number in [1, 3, 7, 10, 42] {
let description = match number {
1 => "one",
2 | 3 => "two or three", // | matches either pattern
4..=9 => "between four and nine", // inclusive range pattern
10 => "ten",
_ => "something else", // _ is the catch-all (required to be exhaustive)
};
println!("{} is {}", number, description);
}
}
$ rustc pattern_matching_match_expressions.rs && ./pattern_matching_match_expressions
1 is one
3 is two or three
7 is between four and nine
10 is ten
42 is something else
Rules:
- Arms are checked top-to-bottom; the first match wins.
matchmust be exhaustive — all possible values must be covered._is the wildcard that matches anything (likedefaultin other languages).- Each arm is
pattern => expression. Multi-line bodies use{}.
Matching Enums
match is at its most powerful when used with enums — it destructures each variant and binds its data to local names in one step. The compiler enforces that every variant is handled, so adding a new variant to an enum immediately produces compile errors in every match that needs updating.
#[derive(Debug)]
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(u8, u8, u8),
}
fn process(msg: Message) {
match msg {
Message::Quit => println!("quit"),
Message::Move { x, y } => println!("move to ({}, {})", x, y),
Message::Write(text) => println!("write: {}", text),
Message::ChangeColor(r, g, b) => println!("color: #{:02X}{:02X}{:02X}", r, g, b),
}
}
fn main() {
process(Message::Move { x: 10, y: 20 });
process(Message::Write(String::from("hello")));
process(Message::ChangeColor(255, 128, 0));
process(Message::Quit);
}
$ rustc pattern_matching_matching_enums.rs && ./pattern_matching_matching_enums
move to (10, 20)
write: hello
color: #FF8000
quit
Matching Option<T> and Result<T, E>
match is the standard way to handle Option and Result. The compiler ensures both the success and failure cases are addressed, eliminating the forgotten-null-check class of bugs.
fn safe_div(a: f64, b: f64) -> Option<f64> {
if b == 0.0 { None } else { Some(a / b) }
}
fn main() {
// Matching Option — handles both Some and None
match safe_div(10.0, 3.0) {
Some(result) => println!("{:.4}", result),
None => println!("division by zero"),
}
// Matching Result — handles both Ok and Err
let parsed: Result<i32, _> = "42".parse();
match parsed {
Ok(n) => println!("parsed: {}", n),
Err(e) => println!("error: {}", e),
}
}
$ rustc pattern_matching_matching_option_t_and_result_t_e.rs && ./pattern_matching_matching_option_t_and_result_t_e
3.3333
parsed: 42
Destructuring in Patterns
Pattern matching is not limited to enums — you can destructure tuples, structs, and nested data in any match arm, binding the inner values to names. This eliminates a lot of the field-access boilerplate you would otherwise write.
Tuples
fn main() {
for point in [(0, 0), (4, 0), (0, -2), (3, -5)] {
match point {
(0, 0) => println!("origin"),
(x, 0) => println!("on x-axis at {}", x), // y matches 0, x is bound
(0, y) => println!("on y-axis at {}", y), // x matches 0, y is bound
(x, y) => println!("at ({}, {})", x, y), // both bound
}
}
}
$ rustc pattern_matching_tuples.rs && ./pattern_matching_tuples
origin
on x-axis at 4
on y-axis at -2
at (3, -5)
Arms are tried top to bottom and the first match wins — (0, 0) would also fit (x, 0), (0, y) and (x, y), but it never reaches them.
Structs
struct Point { x: i32, y: i32 }
fn main() {
for p in [Point { x: 0, y: 9 }, Point { x: 3, y: 0 }, Point { x: 3, y: 7 }] {
match p {
Point { x: 0, y } => println!("on y-axis at {}", y),
Point { x, y: 0 } => println!("on x-axis at {}", x),
Point { x, y } => println!("at ({}, {})", x, y),
}
}
}
$ rustc pattern_matching_structs.rs && ./pattern_matching_structs
on y-axis at 9
on x-axis at 3
at (3, 7)
Nested Enums
Patterns can be nested arbitrarily deep, which lets you match on deeply structured data in a single arm without intermediate variables:
enum Color {
Rgb(u8, u8, u8),
Hsv(u16, u8, u8),
}
enum Background {
Solid(Color),
Gradient(Color, Color),
}
fn main() {
let backgrounds = vec![
Background::Solid(Color::Rgb(255, 0, 128)),
Background::Solid(Color::Hsv(200, 80, 90)),
Background::Gradient(Color::Rgb(0, 0, 0), Color::Hsv(0, 0, 100)),
Background::Gradient(Color::Rgb(9, 9, 9), Color::Hsv(120, 50, 50)),
Background::Gradient(Color::Rgb(9, 9, 9), Color::Rgb(1, 2, 3)),
];
for bg in backgrounds {
match bg {
// One arm can destructure through two levels of nesting
Background::Solid(Color::Rgb(r, g, b)) =>
println!("solid RGB: {} {} {}", r, g, b),
Background::Solid(Color::Hsv(h, s, v)) =>
println!("solid HSV: {} {} {}", h, s, v),
Background::Gradient(Color::Rgb(0, 0, 0), _) =>
println!("gradient from black"),
Background::Gradient(_, Color::Hsv(h, _, _)) =>
println!("gradient ending at hue {}", h),
Background::Gradient(_, _) =>
println!("gradient"),
}
}
}
$ rustc pattern_matching_nested_enums.rs && ./pattern_matching_nested_enums
solid RGB: 255 0 128
solid HSV: 200 80 90
gradient from black
gradient ending at hue 120
gradient
Background::Gradient(Color::Rgb(0, 0, 0), _) matches through two levels and pins literal values: only a gradient that starts at pure black takes that arm, whatever it ends in. The first gradient also ends in an HSV colour, but it never reaches the hue arm: earlier arms win. The plain Gradient(_, _) arm catches whatever is left — _ matches anything without binding it.
Match Guards
A match guard is an extra if condition appended after a pattern. The arm only fires if both the pattern matches and the guard condition is true. Guards let you express conditions that cannot be encoded in the pattern syntax alone.
fn main() {
for num in [Some(-3), Some(0), Some(4), Some(7), None] {
match num {
Some(x) if x < 0 => println!("negative: {}", x),
Some(x) if x == 0 => println!("zero"),
Some(x) if x % 2 == 0 => println!("positive even: {}", x),
Some(x) => println!("positive odd: {}", x),
None => println!("none"),
}
}
}
$ rustc pattern_matching_match_guards.rs && ./pattern_matching_match_guards
negative: -3
zero
positive even: 4
positive odd: 7
none
The compiler does not look inside guards when checking exhaustiveness. Delete the plain Some(x) arm and the match no longer compiles, even though the four guarded arms cover every integer between them.
Binding with @
The @ operator lets you bind a name to a matched value while simultaneously applying a pattern to it. Without @ you would need either a guard (losing the range check in the type) or a separate variable. This is particularly useful with range patterns where you want both the constraint and the value.
fn main() {
for age in [7, 17, 40, 81] {
let category = match age {
n @ 0..=12 => format!("child ({})", n), // n is bound AND must be in 0..=12
n @ 13..=17 => format!("teenager ({})", n),
n @ 18..=64 => format!("adult ({})", n),
n => format!("senior ({})", n), // catch-all, n is bound
};
println!("{}", category);
}
}
$ rustc pattern_matching_binding_with.rs && ./pattern_matching_binding_with
child (7)
teenager (17)
adult (40)
senior (81)
if let — Single-Pattern Match
if let is syntactic sugar for a match with exactly one meaningful arm and an ignored catch-all. Use it when you care about one specific variant and want to silently skip everything else. It produces more readable code than a full match when only one case needs handling.
fn main() {
let config: Option<&str> = Some("verbose");
// Full match — wordy when you only care about one case
match config {
Some(val) => println!("config: {}", val),
None => (), // explicitly doing nothing
}
// if let — cleaner for the single-case scenario
if let Some(val) = config {
println!("config: {}", val);
}
// if let with else — when the missing case also needs handling
if let Some(val) = config {
println!("set to: {}", val);
} else {
println!("not configured");
}
}
$ rustc pattern_matching_if_let_single_pattern_match.rs && ./pattern_matching_if_let_single_pattern_match
config: verbose
config: verbose
set to: verbose
while let — Loop Until Pattern Fails
while let loops as long as a pattern matches, and exits cleanly when it does not. It is the idiomatic way to drain a stack, consume an iterator manually, or process items until a channel closes.
fn main() {
let mut stack = vec![1, 2, 3, 4, 5];
// Loops until pop() returns None (empty vec)
while let Some(top) = stack.pop() {
println!("{}", top);
}
}
$ rustc pattern_matching_while_let_loop_until_pattern_fails.rs && ./pattern_matching_while_let_loop_until_pattern_fails
5
4
3
2
1
let and Function Parameters Destructure Too
Pattern matching is not limited to match, if let, and while let — it is built into let bindings and function parameters. This means you can destructure values wherever you introduce a binding.
fn print_point(&(x, y): &(i32, i32)) {
// The parameter itself is a pattern — destructures the tuple reference on entry
println!("({}, {})", x, y);
}
fn main() {
// let with a tuple — destructures immediately
let (a, b, c) = (1, 2, 3);
println!("{} {} {}", a, b, c);
// let with a struct — destructures named fields
struct Point { x: i32, y: i32 }
let Point { x, y } = Point { x: 5, y: 10 };
println!("{} {}", x, y);
// function parameter destructuring
print_point(&(3, 4));
}
$ rustc pattern_matching_let_and_function_parameters_destructure_.rs && ./pattern_matching_let_and_function_parameters_destructure_
1 2 3
5 10
(3, 4)
Ignoring Values
Rust provides several ways to ignore parts of a matched value when you do not need them:
fn main() {
let numbers = (1, 2, 3, 4, 5);
// .. ignores all middle elements — matches first and last
match numbers {
(first, .., last) => println!("first: {}, last: {}", first, last),
}
// _ ignores specific positions
let (_, second, _, fourth, _) = numbers;
println!("{} {}", second, fourth);
// Prefix _ suppresses the unused variable warning without binding
let _unused = 42;
}
$ rustc pattern_matching_ignoring_values.rs && ./pattern_matching_ignoring_values
first: 1, last: 5
2 4
Practical: Parsing Commands
This example shows how match on a slice of string parts produces clean, readable command dispatch — a common pattern in CLI tools and simple interpreters:
#[derive(Debug)]
enum Command {
Quit,
Go(String),
Set { key: String, value: String },
}
fn parse_command(input: &str) -> Option<Command> {
let parts: Vec<&str> = input.trim().splitn(3, ' ').collect();
match parts.as_slice() {
["quit"] => Some(Command::Quit),
["go", dest] => Some(Command::Go(dest.to_string())),
["set", key, val] => Some(Command::Set {
key: key.to_string(),
value: val.to_string(),
}),
_ => None, // unrecognised input
}
}
fn main() {
let inputs = ["quit", "go north", "set speed 10", "unknown"];
for input in &inputs {
match parse_command(input) {
Some(Command::Quit) => println!("bye"),
Some(Command::Go(dest)) => println!("walking {}", dest),
Some(Command::Set { key, value }) => println!("{} := {}", key, value),
None => println!("unknown command: {}", input),
}
}
}
$ rustc pattern_matching_practical_parsing_commands.rs && ./pattern_matching_practical_parsing_commands
bye
walking north
speed := 10
unknown command: unknown
parts.as_slice() turns the Vec<&str> into a slice so its shape can be matched: ["go", dest] only fits a two-element slice whose first element is "go". splitn(3, ' ') caps the split at three parts, so set title hello world would still match ["set", key, val] with val = "hello world".
Practice
-
In the match-guards example, change the last
Some(x)arm toSome(x) if x % 2 != 0. Every integer is now covered by one of the guards — does it compile?Solution
No:
error[E0004]: non-exhaustive patterns: `Some(_)` not covered ... = note: match arms with guards don't count towards exhaustivityThe compiler does not reason about what guards cover, so at least one unguarded
Some(..)arm (or_) is required. -
In the tuples example, move the
(x, y)arm to the top. What does the compiler warn about, and what prints for(0, 0)?Solution
warning: unreachable pattern ... 4 | (x, y) => println!("at ({}, {})", x, y), | ------ matches any value 5 | (0, 0) => println!("origin"), | ^^^^^^ no value can reach thisIt still compiles, but
(0, 0)printsat (0, 0)— the catch-all wins because it comes first. -
Feed
"set title hello world"and"go"toparse_command. Predict which arms match.Solution
"set title hello world"splits into at most three parts,["set", "title", "hello world"], so it matches["set", key, val]and printstitle := hello world."go"is the one-element slice["go"], which fits neither["quit"]nor["go", dest], so it printsunknown command: go. -
Combine
@with an or-pattern: bindsmallonly when a number is1,2or3, and try it on2,5and9.Solution
match n { small @ (1 | 2 | 3) => println!("{} is small", small), other => println!("{} is not", other), }Prints
2 is small,5 is not,9 is not. The parentheses are needed so@binds the whole alternation, not just the1.