Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Chapter 21: Patterns and Pattern Matching

Patterns are a special syntax in Rust used for matching against the structure of types. They allow you to check if values conform to a certain shape, and if they do, you can bind parts of those values to variables. While most commonly associated with the powerful match expression, patterns are ubiquitous in Rust, appearing also in let statements, function parameters, if let, while let, let else, and for loops.

For C programmers, Rust’s pattern matching, especially within match, significantly extends the capabilities of C’s switch statement. While switch is primarily limited to integers and enum constants, Rust patterns can destructure complex types like structs, tuples, and enums (including those with associated data), match against ranges or literals, handle multiple possibilities in one arm, and apply conditional logic using guards.

This chapter delves into the various forms of patterns, their use cases across the language, and how they compare to C’s switch. Understanding patterns is fundamental to leveraging Rust’s expressiveness and safety features for writing concise and robust code.


21.1 Comparison: C switch vs. Rust match

The switch statement in C provides basic conditional branching based on the value of an expression, but it has several limitations compared to Rust’s match:

  • Limited Types: C’s switch works reliably only with integral types (like int, char) and enumeration constants. It cannot directly handle strings, floating-point numbers, or complex data structures.
  • Fall-through Behavior: By default, execution “falls through” from one case label to the next unless explicitly stopped by a break statement. This is a notorious source of bugs if break is accidentally omitted.
  • Non-Exhaustiveness: The C compiler typically does not enforce that all possible values of an enum or integer range are handled within a switch. While warnings might be available, missing cases can lead to unhandled states and runtime errors.
  • Simple Comparisons: case labels only permit direct equality comparisons against constant values.

Rust’s match expression systematically addresses these points:

  • Type Versatility: match works with any type, including complex data structures like structs, enums, tuples, and slices.
  • Exhaustiveness Checking: The Rust compiler requires that a match expression covers all possible variants for the type being matched (especially enums). This compile-time check eliminates entire classes of bugs related to unhandled cases. The wildcard pattern (_) can be used to explicitly handle any remaining possibilities.
  • No Fall-through: Each arm of a match expression (PATTERN => EXPRESSION) is self-contained. Execution does not automatically fall through to the next arm, preventing related bugs.
  • Powerful Pattern Syntax: match arms use patterns that go far beyond simple equality checks. They can destructure data, bind values to variables, match ranges, combine multiple possibilities (|), and use conditional guards (if condition).
  • Value Binding: Patterns can extract parts of the matched value and bind them to new variables available only within the scope of the matching arm.

Overall, match provides a safer, more expressive, and more versatile tool for control flow based on the structure and value of data compared to C’s switch.


21.2 Overview of Pattern Syntax

Patterns in Rust combine several building blocks:

  • Literals: Match exact constant values (e.g., 42, -1, 3.14, true, 'a', "hello"). Note: Floating-point matching requires specific language features due to equality complexities.
  • Identifiers (Variables): Match any value and bind it to a variable name (e.g., x). If the identifier names a constant, it matches that constant’s value instead of binding.
  • Wildcard (_): Matches any value without binding it. Used to ignore parts or all of a value.
  • Ranges (start..=end): Matches any value within an inclusive range (e.g., 0..=9, 'a'..='z'). Primarily used for char and integer types. Exclusive ranges (..) are not allowed in patterns.
  • Tuple Patterns: Destructure tuples by position (e.g., (x, 0, _), (.., last)).
  • Struct Patterns: Destructure structs by field names (e.g., Point { x, y }, Config { port: 80, .. }). Supports field name punning (x is shorthand for x: x).
  • Enum Patterns: Match specific enum variants, optionally destructuring associated data (e.g., Option::Some(val), Result::Ok(data), Color::Rgb { r, g, b }).
  • Slice & Array Patterns: Match fixed-size arrays or variable-size slices based on elements (e.g., [first, second], [head, ..], [.., last], [a, b, rest @ ..]).
  • Reference Patterns (&, &mut): Match values behind references.
  • ref and ref mut Keywords: Create references to parts of a value being matched, avoiding moves.
  • OR Patterns (|): Combine multiple patterns; if any sub-pattern matches, the arm executes (e.g., ErrorKind::NotFound | ErrorKind::PermissionDenied => ...).
  • @ Bindings (name @ pattern): Bind the entire value matched by a sub-pattern to a variable while also testing against that sub-pattern (e.g., id @ 1..=9).

21.3 Refutable vs. Irrefutable Patterns

A crucial concept is the distinction between refutable and irrefutable patterns:

  • Irrefutable Patterns: These patterns are guaranteed to match any value of the expected type. Examples include binding a variable (let x = value;), destructuring a struct (let MyStruct { field1, field2 } = s;), or a tuple (let (a, b) = tuple;). Irrefutable patterns are required in contexts where a match failure is not meaningful or allowed, such as:

    • let statements
    • Function and closure parameters
    • for loops
  • Refutable Patterns: These patterns might fail to match a given value for a specific type. Examples include matching a literal (42 only matches the value 42), an enum variant (Some(x) doesn’t match None), or a range (1..=5 doesn’t match 6). Refutable patterns are used in contexts designed to handle potential match failures:

    • match expression arms (except potentially the final wildcard _ arm)
    • if let conditions
    • while let conditions
    • let else statements

The compiler enforces this distinction. Trying to use a refutable pattern where an irrefutable one is needed (e.g., let Some(x) = option_value;) results in a compile-time error because the code wouldn’t know what to do if option_value were None.


21.4 Simple let Bindings as Patterns

Even the most basic variable declaration uses an irrefutable pattern:

fn main() {
    let x = 5; // `x` is an irrefutable pattern binding the value 5.
    let point = (10, 20);
    let (px, py) = point;
    // `(px, py)` is an irrefutable tuple pattern destructuring `point`.

    println!("x = {}", x);
    println!("Point coordinates: ({}, {})", px, py);

    struct Dimensions { width: u32, height: u32 }
    let dims = Dimensions { width: 800, height: 600 };
    let Dimensions { width, height } = dims;
    // Irrefutable struct pattern (with punning)
    println!("Dimensions: {}x{}", width, height);
}

These let statements work because the patterns (x, (px, py), Dimensions { width, height }) will always successfully match the type of the value on the right-hand side.


21.5 match Expressions

The match expression is Rust’s primary tool for complex pattern matching. It evaluates an expression and executes the code associated with the first matching pattern arm. A key feature of patterns within match arms (and other pattern contexts) is their ability to bind parts of the matched value to new variables. These bindings are then available within the scope of the corresponding arm’s code block.

match VALUE_EXPRESSION {
    PATTERN_1 => CODE_BLOCK_1, // Variables bound in PATTERN_1 are available here
    PATTERN_2 => CODE_BLOCK_2, // Variables bound in PATTERN_2 are available here
    // ...
    PATTERN_N => CODE_BLOCK_N, // Variables bound in PATTERN_N are available here
}

21.5.1 Example: Matching Option<T>

Handling optional values is a classic use case that demonstrates both pattern matching and variable binding:

fn check_option(opt: Option<&str>) {
    match opt {
        Some(message) => { // `Some(message)` is a pattern.
                          // If `opt` is `Some`, the inner value is bound to `message`.
            println!("Received message: {}", message); // `message` is used here.
        }
        None => { // `None` is a simple pattern matching the None variant.
            println!("No message received.");
        }
    }
}

fn main() {
    check_option(Some("Processing Data")); // Output: Received message: Processing Data
    check_option(None);                    // Output: No message received.
}

In this example, if opt contains a Some variant, the pattern Some(message) matches. The value inside the Some (which is a &str in this case) is bound to the variable message. This message variable is then accessible within the first arm’s code block. If opt is None, the second arm None => ... matches. The compiler ensures all possibilities (Some and None) are handled, guaranteeing exhaustiveness.


21.6 Matching Enums

match is particularly powerful with enums, allowing clean handling of different variants and their associated data.

enum AppEvent {
    KeyPress(char),
    Click { x: i32, y: i32 },
    Quit,
}

fn handle_event(event: AppEvent) {
    match event {
        AppEvent::KeyPress(c) => { // Destructure the char
            println!("Key pressed: '{}'", c);
        }
        AppEvent::Click { x, y } => { // Destructure fields using punning
            println!("Mouse clicked at ({}, {})", x, y);
        }
        AppEvent::Quit => {
            println!("Quit event received.");
        }
    }
}

fn main() {
    handle_event(AppEvent::KeyPress('q'));
    handle_event(AppEvent::Click { x: 100, y: 250 });
    handle_event(AppEvent::Quit);
}

Matching Result<T, E> follows the same principle:

fn divide(numerator: f64, denominator: f64) -> Result<f64, String> {
    if denominator == 0.0 {
        Err("Division by zero".to_string())
    } else {
        Ok(numerator / denominator)
    }
}

fn main() {
    let result1 = divide(10.0, 2.0);
    match result1 {
        Ok(value) => println!("Result: {}", value), // Output: Result: 5
        Err(msg) => println!("Error: {}", msg),
    }

    let result2 = divide(5.0, 0.0);
    match result2 {
        Ok(value) => println!("Result: {}", value),
        Err(msg) => println!("Error: {}", msg), // Output: Error: Division by zero
    }
}

Again, the compiler enforces that both Ok and Err variants are handled.


21.7 Matching Literals, Ranges, Variables, and OR Patterns

Patterns can match specific values, ranges, or combine possibilities:

fn describe_number(n: i32) {
    match n {
        0 => println!("Zero"),
        1 | 3 | 5 => println!("Small odd number (1, 3, or 5)"), // OR pattern `|`
        10..=20 => println!("Between 10 and 20 (inclusive)"), // Range pattern `..=`
        x if x < 0 => println!("Neg. number: {}", x), // Variable binding + Guard `if`
        _ => println!("Other positive number"), // Wildcard `_`
    }
}

fn main() {
    describe_number(0);    // Output: Zero
    describe_number(3);    // Output: Small odd number (1, 3, or 5)
    describe_number(15);   // Output: Between 10 and 20 (inclusive)
    describe_number(-5);   // Output: Negative number: -5
    describe_number(100);  // Output: Other positive number
}
  • Literals: 0 matches the value zero.
  • OR Pattern (|): 1 | 3 | 5 matches if n is 1, 3, or 5.
  • Range Pattern (..=): 10..=20 matches integers from 10 to 20. Works for char too ('a'..='z').
  • Variable Binding: x in x if x < 0 binds the value of n if the guard condition holds.
  • Match Guard (if): The if x < 0 condition must be true for the arm to match.
  • Wildcard (_): Catches any remaining values, ensuring exhaustiveness.

21.8 Ignoring Parts of a Value: _ and ..

Often, you only care about certain parts of a value. Rust provides ways to ignore the rest:

  • _: Ignores a single element or field. Can be used multiple times.
  • _name: A variable name starting with _ still binds the value but signals intent to potentially not use it, suppressing the “unused variable” warning.
  • ..: Ignores all remaining elements in a tuple, struct, slice, or array pattern. Can appear at most once per pattern.
struct Config {
    hostname: String,
    port: u16,
    retries: u8,
}

fn check_port(config: &Config) {
    match config {
        // Match only standard web ports, ignore other fields with `..`
        Config { port: 80 | 443, .. } => {
            println!("Using standard web port: {}", config.port);
        }
        // Match specific hostname, ignore port using `_`, ignore retries with `..`
        Config { hostname: h, port: _, .. } if h == "localhost" => {
             println!("Connecting to localhost on some port.");
        }
        // Ignore the entire struct content
        _ => {
            println!("Using non-standard configuration on host: {}", config.hostname);
        }
    }
}

fn main() {
    let cfg1 = Config { hostname: "example.com".to_string(), port: 80, retries: 3 };
    let cfg2 = Config { hostname: "localhost".to_string(), port: 8080, retries: 5 };
    let cfg3 = Config { hostname: "internal.net".to_string(), port: 9000, retries: 1 };

    check_port(&cfg1); // Output: Using standard web port: 80
    check_port(&cfg2); // Output: Connecting to localhost on some port.
    check_port(&cfg3); // Output: Using non-standard configuration on host: internal.net
}

Using .. is more concise than listing all ignored fields with _, e.g., Config { port: 80, hostname: _, retries: _ }.


21.9 Binding Values While Testing: The @ Pattern

The @ (“at”) operator lets you bind a value to a variable while simultaneously testing it against a pattern.

fn check_error_code(code: u16) {
    match code {
        // Match codes 400-499, bind the matched code to `client_error_code`
        client_error_code @ 400..=499 => {
            println!("Client Error code: {}", client_error_code);
        }
        // Match codes 500-599, bind to `server_error_code`
        server_error_code @ 500..=599 => {
            println!("Server Error code: {}", server_error_code);
        }
        // Match any other code
        other_code => {
            println!("Other code: {}", other_code);
        }
    }
}

fn main() {
    check_error_code(404); // Output: Client Error code: 404
    check_error_code(503); // Output: Server Error code: 503
    check_error_code(200); // Output: Other code: 200
}

Here, client_error_code @ 400..=499 first checks if code is in the range. If yes, the value of code is bound to client_error_code for use within the arm. This is useful when you need the value that matched a specific condition (like a range or enum variant) within the corresponding code block.

It works well with simple values (integers, chars) and enum variants. Matching complex types like String against literals using @ requires care; often, a combination of binding and a match guard is more idiomatic:

fn check_message(opt_msg: Option<String>) {
    match opt_msg {
        // Bind the String to `msg`, then use a guard to check its value
        Some(ref msg) if msg == "CRITICAL" => {
            println!("Handling critical message!");
        }
        // Bind any Some(String) using `ref` to avoid moving the string
        Some(ref msg) => {
            println!("Received message: {}", msg);
        }
        None => {
             println!("No message.");
        }
    }
}
fn main() {
    check_message(Some("CRITICAL".to_string())); // Output: Handling critical message!
    check_message(Some("INFO".to_string()));    // Output: Received message: INFO
    check_message(None);                       // Output: No message.
}

21.10 Match Guards: Adding if Conditions

A match guard is an additional if condition applied to a match arm, placed after the pattern. The arm executes only if the pattern matches and the guard expression evaluates to true.

struct SensorReading {
    id: u32,
    value: f64,
    is_critical: bool,
}

fn process_reading(reading: SensorReading) {
    match reading {
        // Pattern: Matches any SensorReading where is_critical is true
        // Guard: Adds condition on the value
        SensorReading { id, value, is_critical: true } if value > 100.0 => {
            println!("High critical reading from sensor {}: {}", id, value);
        }
        // Pattern: Matches any critical reading (guard already handled high values)
        SensorReading { id, is_critical: true, .. } => {
            println!("Normal critical reading from sensor {}.", id);
        }
        // Pattern: Matches any non-critical reading
        SensorReading { id, value, is_critical: false } => {
             println!("Non-critical reading from sensor {}: {}", id, value);
        }
    }
}

fn main() {
    process_reading(SensorReading { id: 1, value: 105.5, is_critical: true });
    // High critical reading from sensor 1: 105.5
    process_reading(SensorReading { id: 2, value: 50.0, is_critical: true });
    // Normal critical reading from sensor 2.
    process_reading(SensorReading { id: 3, value: 30.0, is_critical: false });
    // Non-critical reading from sensor 3: 30
}

Variables bound in the pattern (like id and value) are available within the guard’s condition. Guards allow expressing conditions that are difficult or impossible to encode directly within the pattern structure itself.


21.11 Destructuring Data Structures

A major strength of patterns is destructuring: breaking down composite types into their constituent parts.

21.11.1 Tuples

fn process_3d_point(point: (i32, i32, i32)) {
    match point {
        (0, 0, 0) => println!("At the origin"),
        (x, 0, 0) => println!("On X-axis at {}", x),
        (0, y, 0) => println!("On Y-axis at {}", y),
        (0, 0, z) => println!("On Z-axis at {}", z),
        (x, y, z) => println!("General point at ({}, {}, {})", x, y, z),
    }
}

fn main() {
    process_3d_point((5, 0, 0)); // Output: On X-axis at 5
    process_3d_point((0, -2, 0)); // Output: On Y-axis at -2
    process_3d_point((1, 2, 3));  // Output: General point at (1, 2, 3)
}

21.11.2 Structs

Use field names to destructure. Field name punning ({ field } for { field: field }) is common.

struct User {
    id: u64,
    name: String,
    is_admin: bool,
}

fn describe_user(user: &User) {
    match user {
        // Use punning for name, specify is_admin, ignore id with `..`
        User { name, is_admin: true, .. } => {
            println!("Admin user: {}", name);
        }
        // Use specific id, pun name, specify is_admin
        User { id: 0, name, is_admin: false } => {
            println!("Special guest user (ID 0): {}", name);
        }
        // Use punning for name, ignore other fields
        User { name, .. } => {
            println!("Regular user: {}", name);
        }
    }
}

fn main() {
    let admin = User { id: 1, name: "Alice".to_string(), is_admin: true };
    let guest = User { id: 0, name: "Guest".to_string(), is_admin: false };
    let regular = User { id: 2, name: "Bob".to_string(), is_admin: false };

    describe_user(&admin);   // Output: Admin user: Alice
    describe_user(&guest);   // Output: Special guest user (ID 0): Guest
    describe_user(&regular); // Output: Regular user: Bob
}

21.11.3 Arrays and Slices

Match fixed-size arrays or variable-length slices by elements.

fn analyze_slice(data: &[u8]) {
    match data {
        [] => println!("Empty slice"),
        [0] => println!("Slice contains only 0"),
        [1, x, y] => println!("Slice starts with 1, followed by {}, {}", x, y),
        // Match first element, ignore middle (`..`), bind last
        [first, .., last] => {
            println!("Slice starts with {} and ends with {}", first, last);
        }
         // Match fixed prefix [0, 1], capture the rest in `tail`
        [0, 1, tail @ ..] => {
             println!("Slice starts [0, 1], rest is {:?}", tail);
        }
        // Fallback using wildcard `_`
        _ => println!("Slice has {} elements, didn't match specific patterns",
        data.len()),
    }
}

fn main() {
    analyze_slice(&[]);          // Output: Empty slice
    analyze_slice(&[0]);         // Output: Slice contains only 0
    analyze_slice(&[1, 5, 8]);   // Output: Slice starts with 1, followed by 5, 8
    analyze_slice(&[10, 20, 30, 40]); // Output: Slice starts with 10 and ends with 40
    analyze_slice(&[0, 1, 2, 3]); // Output: Slice starts [0, 1], rest is [2, 3]
    analyze_slice(&[2, 3]);      // Output: Slice has 2 elements...
}

Key slice/array patterns:

  • [a, b, c]: Matches exactly 3 elements.
  • [head, ..]: Matches 1 or more elements, binds head.
  • [.., tail]: Matches 1 or more elements, binds tail.
  • [first, .., last]: Matches 2 or more elements.
  • [prefix.., name @ .., suffix..]: Captures sub-slices.

21.11.4 Matching References and Using ref/ref mut

When matching references or needing to borrow within a pattern (to avoid moving values), use &, ref, and ref mut.

  1. & in Pattern: Matches a value held within a reference. The pattern &p expects a reference and matches p against the value pointed to.
  2. ref Keyword: Creates an immutable reference (&T) to a field or element within the matched value. Use this when matching by value but need to borrow parts instead of moving them (especially for non-Copy types).
  3. ref mut Keyword: Creates a mutable reference (&mut T). Use this when matching by value or mutable reference and need mutable access to parts without moving.
fn main() {
    // 1. Matching `&` directly
    let reference_to_val: &i32 = &10;
    match reference_to_val {
        // Here, `&10` means: expect `reference_to_val` to be a reference,
        // and check if the value it points to is 10.
        &10 => println!("Value is 10 (matched via &)"),
        _ => {}
    }

    // Example with Option<&T>
    let owned_string = "hello".to_string(); // Create an owned String that lives longer
    let opt_ref: Option<&String> = Some(&owned_string);
    // opt_ref now holds a valid reference

    match opt_ref {
        // `opt_ref` contains an `&String`.
        // The pattern `Some(&ref s)` means:
        // - The `Some` variant is expected.
        // - The `&` in `&ref s` matches the `&String` from `opt_ref`.
        // It effectively says:
        // "The value inside Some is a reference; match against what it
        // *points* to (the String)."
        // - `ref s` then matches against that `String`. Instead of trying
        // to move the `String` (which isn't Copy),
        // `ref s` creates a new reference `s` (of type `&String`) to that `String`.
        Some(&ref s) => println!("Got reference to string: {}", s),
        None => {}
    }


    // 2. Using `ref` to borrow from an owned value being matched
    let maybe_owned_string: Option<String> = Some("world".to_string());
    match maybe_owned_string {
        // `maybe_owned_string` contains a `String`.
        // `Some(s)` would try to move the String out of the Option.
        // `Some(ref s)` makes `s` an `&String`, borrowing from `maybe_owned_string`.
        Some(ref s) => {
            println!("Borrowed string: {}", s);
            // `maybe_owned_string` is still owned and valid here
            // because `s` only borrows.
        }
        None => {}
    }
    // We can still use maybe_owned_string here if it wasn't None
    if let Some(s_val) = &maybe_owned_string { // Borrow for inspection
        println!("Original Option still contains: {}", s_val);
    }


    // 3. Using `ref mut` to modify through a mutable reference
    let mut maybe_count: Option<u32> = Some(5);
    match maybe_count {
        // `maybe_count` contains a `u32`.
        // `Some(ref mut c)` makes `c` an `&mut u32`, mutably borrowing
        // from `maybe_count`.
        Some(ref mut c) => {
            *c += 1;
            println!("Incremented count: {}", c);
        }
        None => {}
    }
    println!("Final count: {:?}", maybe_count); // Output: Final count: Some(6)
}

Using ref and ref mut is essential when destructuring non-Copy types (like String, Vec) from within an owned context (like Option<String>) if you don’t want the pattern matching to take ownership of those parts. When matching against existing references (like &String or &mut T), & in the pattern allows you to “see through” the reference, and ref or ref mut may then be needed to re-borrow the underlying data.


21.12 Matching Smart Pointers like Box<T>

Patterns work naturally with smart pointers like Box<T>. The pointer is often implicitly dereferenced during matching.

enum Data {
    Value(i32),
    Pointer(Box<i32>),
}

fn process_boxed_data(data: Data) {
    match data {
        Data::Value(n) => {
             println!("Got direct value: {}", n);
        }
        // `Box` is implicitly dereferenced to match the inner `i32`.
        // `boxed_val` here binds the `i32` value *inside* the Box.
        // This pattern takes ownership of the Box and thus the value.
        Data::Pointer(boxed_val) => {
            println!("Got value from Box: {}", *boxed_val);
            // `boxed_val` is the `Box<i32>` itself. We can use it here.
        }
    }
}

fn main() {
    let d1 = Data::Value(10);
    let d2 = Data::Pointer(Box::new(20));

    process_boxed_data(d1); // Output: Got direct value: 10
    process_boxed_data(d2); // Output: Got value from Box: 20
    // d2 is moved into the function call and consumed by the matching arm.
}

If you need to match a Box<T> without taking ownership, match a reference to the Data enum and use ref or ref mut on the inner value if needed:

enum Data {
    Value(i32),
    Pointer(Box<i32>),
}

fn inspect_boxed_data_ref(data: &Data) {
    match data { // `data` is a reference `&Data`
        // For `Data::Value(n)`, `n` automatically becomes `&i32` due to
        // match ergonomics.
        Data::Value(n) => println!("Inspecting direct value: {}", n),

        // When matching on `data: &Data`, patterns like `Data::Pointer(field_name)`
        // automatically cause `field_name` to bind as a reference.
        // Here, `boxed_ptr` naturally becomes `&Box<i32>`.
        // The explicit `ref` keyword is therefore unnecessary
        // and disallowed by the compiler.
        Data::Pointer(boxed_ptr) => { // `ref` keyword removed
            // `boxed_ptr` is of type `&Box<i32>`.
            // The first `*` dereferences `&Box<i32>` to `Box<i32>`.
            // The second `*` dereferences `Box<i32>` to `i32`.
            println!("Inspecting value in Box: {}", **boxed_ptr);
        }
    }
}

fn main() {
    let d_box = Data::Pointer(Box::new(30));
    inspect_boxed_data_ref(&d_box); // Output: Inspecting value in Box: 30
    // d_box is still owned here as we passed a reference.
}

(Note: The box keyword for matching directly on heap allocation (box pattern) is still an unstable feature and not recommended for general use.)


21.13 if let and while let: Concise Conditional Matching

When you only care about matching one specific pattern and ignoring the rest, a full match can be verbose. if let and while let provide more concise alternatives.

21.13.1 if let

Handles a single refutable pattern. Executes the block if the pattern matches. Can optionally have an else block for the non-matching case.

fn main() {
    let config_value: Option<i32> = Some(5);

    // Using if let
    if let Some(value) = config_value {
        println!("Config value is: {}", value);
    } else {
        println!("Config value not set.");
    }

    let error_code: Result<u32, &str> = Err("Network Error");
    if let Ok(data) = error_code {
        // This block is skipped
        println!("Operation succeeded: {}", data);
    } else {
        println!("Operation failed."); // This block runs
    }
}

21.13.2 while let

Creates a loop that continues as long as the pattern matches the value produced in each iteration (commonly from an iterator or repeated function call).

fn main() {
    let mut tasks = vec![Some("Task 1"), None, Some("Task 2"), Some("Task 3")];

    // Process tasks from the end using pop() which returns Option<T>
    while let Some(task_option) = tasks.pop() { // Pattern: Some(task_option)
        if let Some(task_name) = task_option { // Nested Pattern: Some(task_name)
             println!("Processing: {}", task_name);
        } else {
             println!("Skipping empty task slot.");
        }
    }
     println!("Finished processing tasks.");
     // Example output order: Task 3, Task 2, Skipping empty task slot, Task 1

    // More direct with `while let Some(Some(..))` pattern:
    let mut data_stream = vec![Some(10), Some(20), None, Some(30)].into_iter();
    // The loop runs as long as `next()` returns `Some(Some(value))`
    while let Some(Some(value)) = data_stream.next() {
         println!("Received value: {}", value); // Outputs 10, 20, 30
    }
    println!("End of stream.");
}

21.14 The let else Construct (Rust 1.65+)

let else allows a refutable pattern in a let binding. If the pattern matches, variables are bound and available in the surrounding scope. If the pattern fails, the else block is executed. Crucially, the else block must diverge (e.g., using return, break, continue, panic!), ensuring control flow doesn’t implicitly continue after a failed match.

fn get_config_param(param_name: &str) -> Option<String> {
    match param_name {
        "port" => Some("8080".to_string()),
        _ => None,
    }
}

fn setup_server() -> Result<(), String> {
    println!("Setting up server...");

    // Use let else to ensure 'port_str' is available or diverge
    let Some(port_str) = get_config_param("port") else {
        // This block executes if get_config_param returns None
        eprintln!("Error: Configuration parameter 'port' not found.");
        return Err("Missing configuration".to_string()); // Diverge by returning Err
    };

    // If we reach here, `port_str` is bound and available
    let port: u16 = port_str.parse().map_err(|_| "Invalid port format".to_string())?;
    println!("Using port: {}", port);

    // ... continue setup with port ...
    Ok(())
}

fn main() {
    match setup_server() {
        Ok(_) => println!("Server setup successful."),
        Err(e) => println!("Server setup failed: {}", e),
    }
}

let else is excellent for early returns or handling errors/missing values concisely at the start of functions or blocks, avoiding deeper nesting than if let or match.


21.15 if let Chains (Rust 2024+)

Stabilized in the Rust 2024 Edition, if let chains (previously known as let_chains) allow combining multiple if let patterns and regular boolean conditions within a single if statement using the logical AND operator (&&).

21.15.1 Motivation

Without let_chains, checking multiple patterns or conditions required nesting:

// Pre-Rust 2024: Nested structure
fn process_nested(opt_a: Option<i32>, opt_b: Option<&str>, flag: bool) {
    if let Some(a) = opt_a {
        if a > 10 {
            if let Some(b) = opt_b {
                 if b.starts_with("prefix") {
                    if flag {
                        println!("All conditions met: a={}, b={}", a, b);
                    }
                 }
            }
        }
    }
}

21.15.2 Example with if let Chains

The if let chain feature significantly improves the ergonomics of handling multiple optional values and conditions. A basic form of if let chains (allowing a single let followed by boolean conditions like if let Some(x) = option && x > 0) was stabilized with Rust 1.76.0 (and is part of the Rust 2024 Edition).

Full support for chaining multiple let expressions together (e.g., if let Some(a) = opt_a && ... && let Some(b) = opt_b && ...), as demonstrated in the example below, has been finalized and is anticipated to be fully available on stable Rust starting with version 1.88 (expected around June 2025).

Therefore, to compile and run the following advanced example using a stable Rust compiler, version 1.88 or newer will be required. If you are using an earlier stable version (such as 1.87.0, which is current in some playgrounds as of May 2025), you will need to switch to the Nightly compiler channel.

The equivalent code becomes much flatter and arguably more readable:

// This advanced `if let` chain syntax, with multiple `let` bindings,
// requires Rust 1.88+ (stable, Rust 2024 Edition).
// If using a Rust stable compiler version prior to 1.88 (e.g., 1.87),
// you will need to switch to the Nightly compiler channel.
fn process_chained(opt_a: Option<i32>, opt_b: Option<&str>, flag: bool) {
    // Combine `if let` and boolean conditions with `&&`
    if let Some(a) = opt_a && a > 10 &&
        let Some(b) = opt_b && b.starts_with("prefix") &&
        flag
    {
        println!("All conditions met: a={}, b={}", a, b);
    } else {
        println!("Conditions not fully met.");
    }
}

fn main() {
    process_chained(Some(20), Some("prefix_data"), true);
    // Output: All conditions met: a=20, b=prefix_data
    process_chained(Some(5), Some("prefix_data"), true);
    // Output: Conditions not fully met. (a > 10 fails)
    process_chained(Some(20), Some("other_data"), true);
    // Output: Conditions not fully met. (b.starts_with fails)
    process_chained(Some(20), Some("prefix_data"), false);
    // Output: Conditions not fully met. (flag fails)
    process_chained(None, Some("prefix_data"), true);
    // Output: Conditions not fully met. (opt_a is None)
}

The conditions are evaluated left-to-right. If any let pattern fails to match or any boolean expression is false, the entire if condition short-circuits to false, and the else block (if present) is executed.

  • Local Development: To compile this example, ensure your project uses the Rust 2024 Edition (edition = "2024" in Cargo.toml) and that your Rust stable compiler is version 1.88 or newer.
  • Online Playgrounds / mdbook: If the environment is using a stable Rust compiler older than 1.88 (e.g., 1.87.0), you will likely encounter a compiler error (E0658). The workaround is to switch the compiler channel in the playground to Nightly. Using #![feature(let_chains)] will not work on stable compilers for this feature

21.16 Patterns in for Loops and Function Parameters

Patterns are also integral to other language constructs.

21.16.1 for Loops

for loops directly use irrefutable patterns to destructure the items yielded by an iterator.

fn main() {
    let coordinates = vec![(1, 2), (3, 4), (5, 6)];
    // `.iter()` yields `&(i32, i32)`.
    // The pattern `(x, y)` destructures each tuple reference.
    // `x` and `y` here will be references (`&i32`) due to iterating over references.
    // To get owned values, use `.into_iter()` on an owned collection.
    for &(x, y) in coordinates.iter() {
    // `&(x, y)` dereferences the item and destructures
        println!("Point: x={}, y={}", x, y); // x, y are i32 here
    }

    let map = std::collections::HashMap::from([("one", 1), ("two", 2)]);
    // Destructuring key-value pairs from HashMap iterator
    for (key, value) in map.iter() { // key is &&str, value is &i32
        println!("{}: {}", key, value);
    }
}

21.16.2 Function and Closure Parameters

Function and closure parameter lists are intrinsically patterns, allowing direct destructuring of arguments.

// Function destructuring a tuple argument
fn print_coordinates((x, y): (f64, f64)) {
    println!("Coordinates: ({:.2}, {:.2})", x, y);
}

// Function ignoring the first parameter
fn process_item(_index: usize, item_name: &str) {
    println!("Processing item: {}", item_name);
}

fn main() {
    print_coordinates((10.5, -3.2));

    process_item(0, "Apple"); // _index is ignored, no unused variable warning

    // Closure parameter destructuring
    let points = [(0, 0), (1, 5), (-2, 3)];
    points.iter().for_each(|&(x, y)| { // `|&(x, y)|` is the closure pattern
        println!("Closure saw point: ({}, {})", x, y);
    });
}

21.17 Nested Patterns

Patterns can be nested to match deeply within complex data structures simultaneously.

enum Status {
    Ok,
    Error(String),
}

struct Response {
    status: Status,
    data: Option<Vec<u8>>,
}

fn handle_response(response: Response) {
    match response {
        // Nested pattern: Match Response struct, then Status::Ok, then Some(data)
        Response { status: Status::Ok, data: Some(payload) } => {
            println!("Success with payload size: {} bytes", payload.len());
            // `payload` is the Vec<u8>
        }
        // Match Ok status, but no data
        Response { status: Status::Ok, data: None } => {
            println!("Success with no data.");
        }
        // Match Error status, bind the message, ignore data field
        Response { status: Status::Error(msg), .. } => {
            println!("Operation failed: {}", msg);
            // `msg` is the String from Status::Error
        }
    }
}

fn main() {
    let resp1 = Response { status: Status::Ok, data: Some(vec![1, 2, 3]) };
    let resp2 = Response { status: Status::Ok, data: None };
    let resp3 = Response { status: Status::Error("Timeout".to_string()), data: None };

    handle_response(resp1); // Output: Success with payload size: 3 bytes
    handle_response(resp2); // Output: Success with no data.
    handle_response(resp3); // Output: Operation failed: Timeout
}

This allows highly specific conditions involving multiple levels of a data structure to be expressed clearly in a single match arm.


21.18 Partial Moves in Patterns

When a pattern destructures a type that does not implement Copy (like String, Vec, Box), binding a field by value moves that field out of the original structure. Rust permits partial moves: moving some fields while borrowing others (ref or ref mut) within the same pattern.

struct Message {
    id: u64,
    content: String, // Not Copy
    metadata: Option<String>, // Not Copy
}

fn main() {
    let msg = Message {
        id: 101,
        content: "Important Data".to_string(),
        metadata: Some("Source=SensorA".to_string()),
    };

    match msg {
        // Move `content`, borrow `id` and `metadata` using `ref`
        Message { id: ref msg_id, content, metadata: ref meta } => {
            println!("Processing message ID: {}", msg_id); // Borrowed `id` as `&u64`
            println!("Moved content: {}", content); // Moved `content`, now owned here
            println!("Borrowed metadata: {:?}", meta);
            // Borrowed `metadata` as `&Option<String>`

            // `msg` itself cannot be fully used after this point because `content`
            // was moved out. Accessing `msg` directly would be a compile error.
            // However, accessing fields *not* moved (like `msg.id` or `msg.metadata`)
            // might theoretically be possible if they weren't also borrowed by `ref`.
            // In practice, you work with the bindings (`msg_id`, `content`, `meta`).
        }
    }

    // Error: `msg` cannot be used here because `msg.content` was moved.
    // println!("Original message ID: {}", msg.id);
    // Compile error: use of partially moved value: `msg`
}

After a partial move, the original variable (msg in this case) is considered “partially moved”. It cannot be used as a whole, preventing potential use-after-move errors for the moved fields. This feature allows fine-grained ownership control during destructuring, potentially avoiding unnecessary clones when only parts of a structure need to be owned.


21.19 Performance Considerations

Rust’s match expressions and pattern matching are designed for efficiency. The compiler translates patterns into optimized low-level code:

  • Jump Tables: For matching enums without associated data, or integers within a dense range, the compiler often generates a jump table (similar to optimized C switch statements), providing O(1) dispatch time.
  • Decision Trees: For more complex patterns involving different types, data destructuring, ranges, or guards, the compiler constructs efficient decision trees using sequences of comparisons and branches.

The overhead of the pattern matching itself is typically minimal compared to the code executed within the match arms. While micro-optimizations are possible, match is generally considered a highly efficient control flow mechanism in Rust. Profiling tools should be used if performance in a specific match expression is critical.


21.20 Summary

Patterns are a fundamental and powerful feature woven throughout Rust, offering significantly more capability than C’s switch. Key advantages include:

  • Safety via Exhaustiveness: The compiler enforces that all possibilities are handled, especially for enums, preventing runtime errors from unhandled cases.
  • Expressive Destructuring: Patterns provide a concise syntax for extracting data from tuples, structs, enums, slices, and more.
  • Versatile Matching: Support for literals, ranges, variables, wildcards (_), OR-patterns (|), @-bindings, references (&, ref, ref mut), and conditional guards (if).
  • Clarity through Refutability: The distinction between irrefutable and refutable patterns guides their correct usage in different contexts (let, match, if let, etc.).
  • Wide Applicability: Patterns are used in match, let, if let, while let, let else, for loops, and function/closure parameters.
  • Advanced Control: Features like partial moves and if let chains provide fine-grained control over ownership and conditional logic.

Understanding and utilizing patterns effectively is crucial for writing idiomatic, robust, and maintainable Rust code. They enable developers to handle complex data structures and control flow logic with clarity and the safety guarantees of the Rust compiler.