Rust track

Lesson 13 · Part 3: Structs, Enums & Traits

Pattern Matching

Match expressions, destructuring and match guards

Part 3: Structs, Enums & Traits4 min readFree lesson

match is Rust's most powerful branching tool. It looks similar to C/Java's switch, but it has an exhaustiveness check, so a compile error occurs if all possibilities are not handled. You can also deconstruct and bind data within patterns, making it naturally suitable for handling enums and Option. if let/while let are shorthand syntaxes for handling only one variant briefly.

Learning Objectives

  • Uses all branching, binding, wildcards, and guards of match.
  • Understands that thanks to the exhaustiveness check, missing branches are caught at compile time.
  • Writes code for a single variant briefly using if let.
  • Handles loops returning an Option naturally with while let.

Key Concepts

1) Basic Structure of match

match value {
Pattern1 => Expression1,
Pattern2 => Expression2,
_    => DefaultExpression, //The rest
}
  • All branches must return the same type of value (expression).
  • Exhaustiveness: compile error if not all possibilities are handled. _ wildcard is a safety net.

2) Data Binding

enum Coin { Quarter(String), /* ... */ }

match c {
    Coin::Quarter(state) => println!("from {state}"),
    _ => (),
}

Assign a name to the internal data of a variant to use it within a branch.

3) Guard if

match score {
    s if s >= 90 => 'A',
    s if s >= 80 => 'B',
    _ => 'F',
}

You can attach additional conditions separate from the pattern. Only the first branch whose guard passes is selected.

4) if let

if let Some(n) = maybe {
    println!("{n}");
}

It's the same as match maybe { Some(n) => ..., None => () } but shorter and clearer. else clause is also possible.

5) while let

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

The loop ends the moment matching fails. It naturally combines with a method that returns Option.

Examples

Example 1 — ex01_match.rs: match, binding, guard

//Example 1 of Chapter 13: match — pattern branching, binding, wildcard, guard

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(String), //Which state's quarter
}

fn describe(c: &Coin) -> String {
    match c {
        Coin::Penny => String::from("1 cent"),
        Coin::Nickel => String::from("5 cents"),
        Coin::Dime => String::from("10 cents"),
        //Bind the internal data of variant
        Coin::Quarter(state) => format!("25 cents ({state} state)"),
    }
}

fn grade(score: i32) -> char {
    //_ wildcard + guard (if condition)
    match score {
        s if s >= 90 => 'A',
        s if s >= 80 => 'B',
        s if s >= 70 => 'C',
        _ => 'F',
    }
}

fn main() {
    println!("{}", describe(&Coin::Penny));
    println!("{}", describe(&Coin::Quarter(String::from("Alaska"))));

    for s in [95, 82, 73, 50] {
        println!("{s} → {}", grade(s));
    }
}

Example 2 — ex02_if_let.rs: if let

//Example 2 of Chapter 13: if let — concise syntax when handling only one variant
//
//Compared to match, suitable when 'ignoring the rest' feels natural.

fn main() {
    let maybe = Some(42);

    //Handled with match
    match maybe {
        Some(n) => println!("(match) value = {n}"),
        None => {}
    }

    //Same operation shortened with if let
    if let Some(n) = maybe {
        println!("(if let) value = {n}");
    }

    //else is also possible
    let nothing: Option<i32> = None;
    if let Some(n) = nothing {
        println!("value = {n}");
    } else {
        println!("(if let else) no value");
    }
}

Example 3 — ex03_while_let.rs: while let

//Example 13-3: while let — Repeat while a pattern matches
//
//It works naturally with methods that return an Option, like Vec's pop().

fn main() {
    let mut stack = vec![1, 2, 3, 4];

    //Remove the last element until the stack is empty.
    while let Some(top) = stack.pop() {
        println!("Popped: {top}");
    }
    println!("Final = {:?}", stack);
}

Common Mistakes

Q. Shouldn't we always put a _ branch in match?

A. Having _ satisfies exhaustiveness, but the compiler won't notify you if a new variant is added. There is a risk of potential missed handling, so list specific variants if possible.

Q. Which is better, if let or match?

A. If there are 2 or more variants and all need to be handled, use match. If you only handle one variant and ignore the rest, use if let. Readability is the deciding factor.

Q. Can I create new variables inside match?

A. The branch body is an ordinary code block, so you can freely use let. Variables bound in the pattern itself are only valid within the branch body.

Q. Does a guard (if) perform exhaustiveness checking?

A. A guard checks an additional condition after pattern matching, so it does not affect exhaustiveness. Even if all guards evaluate to false, the compiler cannot know, so it is good practice to place a safety net like _ at the end when using guards.

Summary

  • match is a powerful branching tool that supports exhaustiveness, binding, and guards.
  • if let is a shorthand syntax that handles only one variant briefly.
  • while let is a repetitive pattern combined with methods that return an Option.
  • Wildcard _ is convenient, but be aware that you may lose notifications when adding new variants.

Try it yourself

Solve the assignment below. The reference solution is shown below.

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/pattern_match/Cargo.tomlTOML
[package]
name = "pattern_match"
version = "0.1.0"
edition = "2021"
default-run = "pattern_match"

[dependencies]
src/pattern_match/src/main.rsRust
fn main() {
    println!("This crate has three executable examples.");
    println!();
    println!("  cargo run --bin ex01_match");
    println!("  cargo run --bin ex02_if_let");
    println!("  cargo run --bin ex03_while_let");
}

Assignment

Problem 1 — Determining Day of the Week and Business Day

  • File name: homework_01/src/main.rs
  • Core concepts: ZX0000ZX, ZX0001ZX pattern combination, guards

Requirements

  • Define enum ZX0000ZX as ZX0001ZX.

  • Function ZX0000ZX returns

  • "Weekday" if it is a weekday (Mon-Fri)

  • "Weekend" if it is a weekend (Sat, Sun)

  • In main, print all 7 days of the week line by line.

Expected output

Mon → Weekday
Tue → Weekday
Wed → Weekday
Thu → Weekday
Fri → Weekday
Sat → Weekend
Sun → Weekend

Hint

  • Day::Mon | Day::Tue | Day::Wed | Day::Thu | Day::Fri => "Weekday".
  • Since all 7 are handled, a wildcard for ZX0000ZX is unnecessary (recommended).
  • If you prefix ZX0000ZX and print as ZX0001ZX, the variable name itself will appear.

Problem 2 — Summing Option Vec

  • File name: homework_02/src/main.rs
  • Core concepts: ZX0000ZX, iteration

Requirements

  • Input: ZX0000ZX
  • Sum and print only the values that are ZX0000ZX.

Expected output

Sum of some values = 16

Hint

  • for v in &values { if let Some(n) = v { total += n; } }.
  • A one-liner solution like ZX0000ZX is also possible — challenge!

Check your answers

Try solving it yourself first, then compare your answer with the reference solution below.

Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

homework_01/Cargo.tomlTOML
[package]
name = "homework_01"
version = "0.1.0"
edition = "2021"

[dependencies]
homework_01/src/main.rsRust
//Key Points:
//- Multiple patterns can be grouped into one branch with `|`.
//- If you write down all variants without omission, the compiler will notify you when a new variant is added.

#[derive(Debug)]
enum Day {
    Mon,
    Tue,
    Wed,
    Thu,
    Fri,
    Sat,
    Sun,
}

fn kind(d: &Day) -> &'static str {
    match d {
        Day::Mon | Day::Tue | Day::Wed | Day::Thu | Day::Fri => "Weekday",
        Day::Sat | Day::Sun => "Weekend",
    }
}

fn main() {
    let week = [Day::Mon, Day::Tue, Day::Wed, Day::Thu, Day::Fri, Day::Sat, Day::Sun];
    for d in &week {
        println!("{:?} → {}", d, kind(d));
    }
}
homework_02/Cargo.tomlTOML
[package]
name = "homework_02"
version = "0.1.0"
edition = "2021"

[dependencies]
homework_02/src/main.rsRust
//Key Points:
//- `if let Some(n) = v` executes the body only if v is Some.
//- `iter().flatten()` automatically filters out None in a sequence of Option<T>.

fn main() {
    let values: Vec<Option<i32>> = vec![Some(1), None, Some(3), Some(5), None, Some(7)];

    let mut total = 0;
    for v in &values {
        if let Some(n) = v {
            total += n;
        }
    }
    println!("Sum of Some values = {total}");

    //Can also be solved in one line (for reference)
    // let total: i32 = values.iter().flatten().sum();
}

Learn this lesson with a mentor

Work through the Rust track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now