Patterns and Matching in Rust

Patterns are a special syntax in Rust used to match against the structure of types, both simple and complex. They are a fundamental part of the language, enabling powerful and expressive control flow, especially when combined with the match keyword. Understanding patterns is key to writing idiomatic Rust code.

Where Patterns Are Used

Patterns appear in several places in Rust, but they all work the same way: they attempt to match a value against a specified structure.

  1. match arms: The most common place to see patterns.
  2. let statements: let PATTERN = EXPRESSION;
  3. if let and while let: For handling a single match case.
  4. for loops: for PATTERN in EXPRESSION
  5. Function parameters: fn foo(PATTERN: Type)

The match Expression

The match expression is the primary way to use patterns. It compares a value against a series of patterns and executes the code associated with the first one that matches. The Rust compiler ensures that match expressions are exhaustive, meaning you must cover every possible case for the value’s type.

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

fn main() {
    let msg = Message::ChangeColor(0, 160, 255);

    match msg {
        Message::Quit => {
            println!("The Quit variant has no data to destructure.");
        }
        Message::Move { x, y } => {
            println!("Move in the x direction {} and y direction {}", x, y);
        }
        Message::Write(text) => {
            println!("Text message: {}", text);
        }
        Message::ChangeColor(r, g, b) => {
            println!("Change the color to red {}, green {}, and blue {}", r, g, b);
        }
    }
}

Conditional if let

Sometimes, a match expression is more verbose than you need, especially if you only care about one specific case. For this, Rust provides if let.

// Using match
let config_max = Some(3u8);
match config_max {
    Some(max) => println!("The maximum is configured to be {}", max),
    _ => (), // We have to handle the None case, even if we do nothing.
}

// Using if let is more concise.
if let Some(max) = config_max {
    println!("The maximum is configured to be {}", max);
}

You can also use if let with an else block, which is equivalent to the _ arm in a match.

A Catalog of Pattern Syntax

Rust’s pattern syntax is rich. Here are the different forms you can use.

1. Literals

You can match against simple literal values.

let x = 1;
match x {
    1 => println!("one"),
    2 => println!("two"),
    _ => println!("anything"),
}

2. Destructuring

Patterns can be used to break down structs, enums, and tuples into their constituent parts.

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    // Destructuring a struct
    let p = Point { x: 0, y: 7 };
    let Point { x, y } = p;
    assert_eq!(0, x);
    assert_eq!(7, y);

    // Destructuring in a match
    match p {
        Point { x, y: 0 } => println!("On the x axis at {}", x),
        Point { x: 0, y } => println!("On the y axis at {}", y),
        Point { x, y } => println!("On neither axis: ({}, {})", x, y),
    }
}

3. Ignoring Values

You can ignore parts of a value that you don’t need.

  • _: Ignores a single value.
  • ..: Ignores all remaining parts of a struct, tuple, or slice.
fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (first, _, third, _, fifth) => {
            println!("Some numbers: {}, {}, {}", first, third, fifth);
        }
    }

    struct Point { x: i32, y: i32, z: i32 }
    let origin = Point { x: 0, y: 0, z: 0 };

    match origin {
        Point { x, .. } => println!("x is {}", x),
    }
}

4. Match Guards

A match guard is an additional if condition specified after a pattern in a match arm. It provides a way to add more complex logic to a pattern.

let num = Some(4);

match num {
    Some(x) if x < 5 => println!("less than five: {}", x),
    Some(x) => println!("{}", x),
    None => (),
}

5. @ Bindings

The @ operator (pronounced “at”) lets you create a variable that holds a value at the same time you are testing that value to see if it matches a pattern.

enum Message {
    Hello { id: i32 },
}

let msg = Message::Hello { id: 5 };

match msg {
    Message::Hello {
        // Bind the value of the `id` field to `id_variable`
        // while also testing that it falls within the range 3..=7.
        id: id_variable @ 3..=7,
    } => {
        println!("Found an id in range: {}", id_variable);
    }
    Message::Hello { id: 10..=12 } => {
        println!("Found an id in another range");
    }
    Message::Hello { id } => {
        println!("Found some other id: {}", id);
    }
}

Conclusion

Patterns are a versatile and powerful feature in Rust. They provide a way to destructure data and control program flow with compile-time guarantees of exhaustiveness. Mastering patterns will allow you to write code that is not only more expressive and concise but also safer and easier to reason about.