Patterns and destructuring

Rust · Essentials · 10 min

Patterns appear in let, match, function parameters and loops; fluency with them makes Rust code short and clear.

A pattern describes the shape of a value and binds parts of it to names. You have already used them in match and if let, but they also appear in let, for, while let and function parameters.

Destructuring:

  • Tuples: let (x, y) = point;, for (k, v) in &map.
  • Structs: let User { name, age, .. } = user; where .. ignores the remaining fields.
  • Slices: match xs { [] => ..., [one] => ..., [first, .., last] => ... }.
  • Function parameters: fn dist(&(x, y): &(f64, f64)).

In a match, patterns can combine alternatives with | (1 | 2 => ...), match ranges ('a'..='z' or 0..=9), and add a match guard with if: Some(n) if n > 100 => .... A guard is checked after the pattern matches; if false, matching continues with the next arm. Note that the compiler does not consider guards when checking exhaustiveness, so you often still need a final arm.

The @ operator binds a name while also testing a pattern: n @ 1..=9 => println!("digit {n}").

_ ignores one value without binding it; _name binds but suppresses the unused warning; .. ignores the rest.

Patterns in let must be irrefutable (always match), like a tuple. Patterns that can fail, like Some(x), need if let, while let, let ... else or match.

Matching on a reference binds references to the parts (called default binding modes), so match &user lets you look at fields without moving them. Use ref/ref mut only in the rare cases this does not cover.

Example

struct Point { x: i32, y: i32 }

fn classify(p: &Point) -> String {
    match p {
        Point { x: 0, y: 0 } => "origin".into(),
        Point { x, y: 0 } | Point { x: 0, y: x } => format!("on an axis at {x}"),
        Point { x, y } if x == y => "on the diagonal".into(),
        Point { x: px @ 1..=9, .. } => format!("small positive x = {px}"),
        _ => "elsewhere".into(),
    }
}

fn main() {
    for p in [Point { x: 0, y: 0 }, Point { x: 0, y: 5 }, Point { x: 3, y: 3 }, Point { x: 4, y: 7 }] {
        println!("{}", classify(&p)); // origin / on an axis at 5 / on the diagonal / small positive x = 4
    }
    let scores = vec![90, 72, 85];
    if let [first, .., last] = scores.as_slice() {
        println!("{first}..{last}"); // 90..85
    }
    let mut stack = vec![1, 2, 3];
    while let Some(top) = stack.pop() { print!("{top} "); } // 3 2 1
}

Remember

  • Patterns work in let, match, if let, while let, for and parameters
  • Use |, ranges, .., guards and @ to express matches precisely
  • let needs irrefutable patterns; refutable ones need if let, let-else or match

Watch out for

  • Expecting the compiler to treat guarded arms as exhaustive; it never does.

Check yourself

  1. Given `match n { x if x < 0 => "neg", 0 => "zero" }` on an i32, why does it fail to compile?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: doc.rust-lang.org/reference/patterns.html