The match Control Flow Construct in Rust
Master Rust match expression: exhaustive pattern matching, binding to enum data, Option handling, and catch-all patterns.
Rust has an extremely powerful control flow construct called match. It allows you to compare a value against a series of patterns and execute code based on which pattern matches first.
Explain Like I’m 10: The Coin-Sorting Machine#
Think of match like a coin-sorting machine:
- You drop a coin into the top slot.
- The coin rolls down tracks with holes of various sizes.
- When it encounters the first hole that fits its exact dimensions, it falls through into that specific bin.
- Each coin triggers an action only for the bin it falls into.
graph TD
Input["Input Value"] --> Arm1{"Matches Pattern 1?"}
Arm1 -- Yes --> Action1["Execute Arm 1 & Return Result"]
Arm1 -- No --> Arm2{"Matches Pattern 2?"}
Arm2 -- Yes --> Action2["Execute Arm 2 & Return Result"]
Arm2 -- No --> ArmCatch{"Matches Catch-all (_ / other)?"}
ArmCatch -- Yes --> ActionCatch["Execute Default Arm"]
In Rust, the compiler makes this even safer: it forces you to handle every possible coin type (exhaustiveness checking), ensuring your program never crashes on unexpected inputs.
Basic match Syntax#
A match expression is composed of match arms. An arm has two parts: a pattern to match against and the code to run when matched, separated by the => operator.
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => {
println!("Lucky penny!");
1
}
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
fn main() {
let coin = Coin::Penny;
println!("Value: {} cents", value_in_cents(coin));
}rust[!NOTE]
matchis an expression in Rust, meaning it returns a value. All match arms must return values of the same type.
Patterns that Bind to Values#
Match arms can bind to the inner values stored inside enum variants, making it easy to extract and inspect data:
#[derive(Debug)]
enum UsState {
Alabama,
Alaska,
California,
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState), // Variant holding embedded data
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {state:?}!");
25
}
}
}
fn main() {
let quarter = Coin::Quarter(UsState::Alaska);
println!("Value: {} cents", value_in_cents(quarter));
}rustWhen passing Coin::Quarter(UsState::Alaska), the state variable binds to UsState::Alaska in that match arm.
Matching with Option<T>#
Rust replaces null with Option<T> (Some(T) or None). Using match is the canonical way to safely unpack and transform Option values:
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
fn main() {
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
println!("five + 1 = {six:?}");
println!("none + 1 = {none:?}");
}rustMatches Are Exhaustive & Catch-all Patterns#
Rust requires match arms to be exhaustive: all possibilities must be accounted for. Missing a variant causes a compilation error:
// Compile error: pattern `None` not covered!
fn plus_one_invalid(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}rustUsing Catch-all Variables and _#
When you only care about specific cases and want a default action for everything else:
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
other => move_player(other), // Binds any other value to variable 'other'
}
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => reroll(), // Matches any other value without binding
}
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => (), // Do nothing (returns unit value)
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(_num: u8) {}
fn reroll() {}rust[!TIP] Catch-all patterns (
otheror_) must always be placed at the very end of the match block, as arms are evaluated in order.
Summary#
matchcompares a value against patterns sequentially and runs the first matching arm.- Arms can destructure and bind to data inside enum variants.
- The compiler guarantees exhaustiveness, eliminating unhandled edge cases.
- Use
_or variable catch-alls at the end when you don’t need to list every specific case.