Pattern Matching in Rust

An introduction to the complexities of pattern matching in Rust.

Explore the differences between matching Copy types, non-Copy types, references, and how Rust’s match ergonomics affects these.

Source

Find source code on GitHub.

Copy Types

Copy types are easy to match. They are on the stack and are always copied so ownership is not a concern.

Copy

Here is an example of matching a Some type in an Option:

let maybe_number = Some(42);
match maybe_number {
    Some(x) => println!("Found a specific value: {}", x),
    None => println!("Found nothing"),
}

and a None type in an Option:

// Must specify type - Rust can't deduce a type from None
let maybe_number: Option<i32> = None;
match maybe_number {
    Some(x) => println!("Found a specific value: {}", x),
    None => println!("Found nothing"),
}

Borrow

References can be matched using Rust’s match ergonomics.

When the match expression is a reference (&maybe_number) and the pattern is not a reference (Some(borrowed)), Rust will:

  • pattern-match the Option as a reference
  • bind the Option’s value as a reference’
let maybe_number = Some(42);
match &maybe_number {
    Some(borrowed) => {
        let x = *borrowed;
        println!("Found something: {}", x);
    },
    None => println!("Found nothing"),
}

That is, Some(borrowed) is treated as &Some(ref borrowed). Similarly, None is treated as &None.

Old-style Borrow

Before match ergonomics, one had to explicitly:

  • match against a reference
  • bind the Option’s value as a reference using the ref keyword

For example:

let maybe_number = Some(42);
let maybe_number_ref = &maybe_number;
match maybe_number_ref {
    &Some(ref borrowed) => {
        let x = *borrowed;
        println!("Found something: {}", x);
    },
    &None => println!("Found nothing"),
}

Alternatively, one could:

  • dereference before matching
  • bind the Option’s value as a reference using the ref keyword
let maybe_number = Some(42);
let maybe_number_ref = &maybe_number;
match *maybe_number_ref {
    Some(ref borrowed) => {
        let x = *borrowed;
        println!("Found something: {}", x);
    },
    None => println!("Found nothing"),
}

Non-Copy Types

Ownership must be considered when matching Non-Copy types. These live on the heap and are either be moved or borrowed.

Move

By default, match statements move/own the matched value. For example,

let maybe_number = Some(Box::new(42));
match maybe_number {
    Some(owns_box) => {
        let x = *owns_box;
        println!("Found something: {}", x);
    },
    None => println!("Found nothing");
}

Using the value again causes a compiler error:

match maybe_number {
    Some(_) => {},
    None => {},
}
Some(owns_box) => {
     -------- value moved here

match maybe_number {
      ^^^^^^^^^^^^ value used here after partial move

Borrow

A matched value can be borrowed instead of owned.

Using match ergonomics, borrowing can be done simply using the & operator:

match &maybe_number {
    Some(borrows_box) => {
        let x = **borrows_box;
        println!("Found something: {}", x);
    },
    None => println!("Found nothing"),
}

This uses a reference match expression (&maybe_number) and a non-reference pattern (Some(borrows_box)), so Rust will use match ergonomics to:

  • pattern-match the Option as a reference
  • bind the Option’s value as a reference

That is, Some(borrows_box) is treated as &Some(ref borrows_box). Similarly, None is treated as &None.

Note the double dereference on the borrowed value; the first * dereferences the borrow, the second gets the value out of the Box (using the Deref trait).

The Box is borrowed so we can match again:

match &maybe_number {
    Some(_) => {},
    None => {},
}

Note that the double dereference must be done in-place to avoid a “move out of borrowed content” compiler error:

match &maybe_number {
    Some(borrows_box) => {
        let the_box = *borrows_box;   // <-- error
        let x = *the_box;
        println!("Found something: {}", x);
    },
    None => println!("Found nothing"),
}

This assignment attempts to move the Box out of the Option and into a new variable. This is not allowed because the Option’s value is borrowed.

Old-style Borrow

Before match ergonomics, one had to explicitly:

  • match against a reference
  • bind the Option’s value as a reference using the ref keyword

For example:

let maybe_number = Some(Box::new(42));
let maybe_number_ref = &maybe_number;
match maybe_number_ref {
    &Some(ref borrows_box) => {
        let x = **borrows_box;
        println!("Found something: {}", x);
    },
    &None => println!("Found nothing"),
}

Alternatively, one could:

  • dereference before matching
  • bind the Option’s value as a reference using the ref keyword
let maybe_number = Some(Box::new(42));
let maybe_number_ref = &maybe_number;
match *maybe_number_ref {
    Some(ref borrows_box) => {
        let x = **borrows_box;
        println!("Found something: {}", x);
    },
    None => println!("Found nothing"),
}

Further Reading

Rust sources:

Stack Overflow: