Skip to main content

Pattern Matching

Pattern matching lets Rust compare a value against a pattern, extract data from it, and decide what to do safely and exhaustively.

Think of it as:

A supercharged switch statement that understands structure, ownership, and types.

let number = 3;

match number {
1 => println!("One"),
2 => println!("Two"),
3 => println!("Three"),
_ => println!("Something else"),
}
  • Rust checks each arm top to bottom
  • The first matching pattern runs
  • _ is a catch-all
  • match must be exhaustive

Why Pattern Matching Matters

Rust data structures are often shaped, not flat: Tuples, Structs, Enums (Option, Result), Slices

Pattern matching lets you look inside these shapes safely.

Matching Enums (Most Important Use Case)

Example: Enum with Data

enum Message {
Quit,
Write(String),
Move { x: i32, y: i32 },
}

Matching Variants

fn handle_message(msg: Message) {
match msg {
Message::Quit => println!("Quit"),
Message::Write(text) => println!("Text: {}", text),
Message::Move { x, y } => println!("Move to ({}, {})", x, y),
}
}

Why this is powerful:

  • Rust forces you to handle every variant
  • Data is safely extracted
  • No null checks, no invalid states

Pattern Matching with Option<T>

Definition Reminder

enum Option<T> {
Some(T),
None,
}

Example

let name = Some("Rust");

match name {
Some(value) => println!("Name: {}", value),
None => println!("No name provided"),
}

Rust won’t let you “forget” the None case. That’s deliberate.

Pattern Matching with Result<T, E>

fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err(String::from("Division by zero"))
} else {
Ok(a / b)
}
}

match divide(10, 2) {
Ok(value) => println!("Result: {}", value),
Err(err) => println!("Error: {}", err),
}

Pattern matching turns error handling into explicit logic, not hope.

Matching Tuples

let point = (0, 5);

match point {
(0, y) => println!("On Y axis at {}", y),
(x, 0) => println!("On X axis at {}", x),
(x, y) => println!("Point at ({}, {})", x, y),
}

Patterns describe structure, not just values.

Matching Structs

struct User {
name: String,
age: u32,
}

let user = User {
name: String::from("Alice"),
age: 20,
};

Destructuring in match

match user {
User { name, age: 18 } => println!("Just became an adult: {}", name),
User { name, age } => println!("{} is {} years old", name, age),
}

Field names become variables automatically.

Ignoring Values with _ and ..

Ignore Single Values

let (_, y) = (10, 20);

Ignore Remaining Fields

match user {
User { name, .. } => println!("User: {}", name),
}

This keeps patterns readable and future-proof.

Match Guards (if Conditions)

Patterns can have extra conditions.

let num = Some(4);

match num {
Some(x) if x % 2 == 0 => println!("Even number"),
Some(_) => println!("Odd number"),
None => println!("No number"),
}

Guards refine patterns without nesting logic.

if let – Pattern Matching Lite

Use if let when you care about one pattern only.

let msg = Message::Write(String::from("Hello"));

if let Message::Write(text) = msg {
println!("Message: {}", text);
}

Less verbose, less exhaustive—use wisely.

while let – Pattern Matching in Loops

let mut stack = vec![1, 2, 3];

while let Some(value) = stack.pop() {
println!("{}", value);
}

The loop ends naturally when the pattern no longer matches.

Matching Slices and Arrays

let numbers = [1, 2, 3];

match numbers {
[1, _, 3] => println!("Matched pattern"),
_ => println!("No match"),
}

Variable-Length Slice Pattern

match &numbers[..] {
[first, middle @ .., last] =>
println!("First: {}, Last: {}", first, last),
}

This is structural reasoning, not indexing.

Ownership and Pattern Matching

Patterns can move, borrow, or copy values.

let msg = Message::Write(String::from("Hello"));

match msg {
Message::Write(text) => println!("{}", text), // moved
_ => (),
}

To avoid moving:

match &msg {
Message::Write(text) => println!("{}", text),
_ => (),
}

Summary

  • Pattern matching inspects structure, not just values
  • match must be exhaustive
  • Works deeply with enums, structs, tuples, slices
  • Enforces correctness at compile time
  • Replaces null checks and fragile branching