rust-pattern-matching-match

Pattern Matching in Rust with match and Destructuring

  • 5 min

The pattern matching consists of comparing a value with patterns and extracting its internal data.

In the previous article, we saw how to store complex data inside an Enum. We were left with a pending question: how do we extract that data?

If I have a variable message that can be Move {x, y} or Write(text), how do I access x or text? We can’t use message.x because if the variable were of type Write, x wouldn’t exist and the program would crash.

Rust solves this with pattern matching and its main tool: the match expression.

We can think of match as a much more expressive version of the switch in C or Java.

Basic Syntax

A match block takes an expression and compares it against a series of patterns. When it finds one that “fits”, it executes the associated code.

Let’s revisit our simple enum of directions:

enum Direction {
    North,
    South,
    East,
    West,
}

fn destination(direction: Direction) {
    match direction {
        Direction::North => println!("Heading towards the cold"),
        Direction::South => println!("Heading to the warmth"),
        Direction::East => println!("Towards where the sun rises"),
        Direction::West => println!("Towards where it sets"),
    }
}
Copied!

The syntax is Pattern => Code. If the code is short, it can be on one line with ,. If it’s complex, we use braces {}.

Exhaustiveness

With this, match sweeps away the traditional switch. In C++, if you forget a case in a switch, the program compiles and simply does nothing (which is often a bug).

In Rust, match must be exhaustive. You must cover every single possibility.

If we comment out a line:

match direction {
    Direction::North => println!("North"),
    Direction::South => println!("South"),
    // We forgot East and West
}
// ❌ Compilation error: pattern `East` not covered
Copied!

The compiler is your safety net. It guarantees you will never have an unhandled state in your logic.

The Wildcard _

Sometimes you don’t want to write a case for every variant (imagine an enum with 100 options, or an integer). For that, we have the wildcard pattern _, which means “anything else”.

let number = 7;

match number {
    1 => println!("One"),
    2 => println!("Two"),
    3 => println!("Three"),
    _ => println!("Any other number"), // Mandatory to be exhaustive
}
Copied!

Destructuring

This is where the real power lies. The match can “unwrap” the enum and extract the values it holds inside through destructuring.

Let’s go back to our message system example:

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

fn process(msg: Message) {
    match msg {
        Message::Quit => {
            println!("Goodbye!");
        },
        Message::Move { x, y } => {
            // Here we have created two valid variables 'x' and 'y'
            println!("Moving to coordinates: {}, {}", x, y);
        },
        Message::Write(text) => {
            // We extract the String into the variable 'text'
            println!("Message received: {}", text);
        },
        Message::ChangeColor(r, g, b) => {
            println!("Changing color to RGB({}, {}, {})", r, g, b);
        }
    }
}
Copied!

Notice the elegance.

  1. We check which variant it is.
  2. We extract the internal data into variables.
  3. We use those variables. All in a single step with guaranteed type safety.

Matching with Option<T>

As we mentioned, match is the standard way to handle Option (the replacement for null).

Imagine a function that returns an Option<i32>.

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None, // If there is nothing, we return nothing
        Some(i) => Some(i + 1), // If there is a number 'i', we add 1
    }
}

fn main() {
    let five = Some(5);
    let six = plus_one(five); // Some(6)
    let nothing = plus_one(None);  // None
}
Copied!

This pattern is so common that Rust has shortcuts for it, like if let, and methods like map that express the same idea without writing a full match.

match is an Expression

Like if, match is an expression. This means it returns a value, and we can assign it to a variable.

let boolean = true;

let number = match boolean {
    true => 1,
    false => 0,
}; // <-- The semicolon goes here

println!("The number is {}", number);
Copied!

For this to work, all branches (arms) of the match must return the same data type. You cannot return a 1 in one branch and a "hello" in another.

Other Patterns

Rust’s pattern system is very deep. Here are some extra tricks:

Multiple Patterns (|)

You can match multiple options on a single line using the OR operator |.

match x {
    1 | 2 => println!("One or Two"),
    3 => println!("Three"),
    _ => println!("Other"),
}
Copied!

Ranges (..=)

You can use ranges in patterns (especially useful with characters or numbers).

let x = 5;
match x {
    1..=5 => println!("Between 1 and 5"),
    _ => println!("Other"),
}
Copied!

Guards (if)

You can add an extra condition to a pattern.

let even = Some(4);

match even {
    Some(x) if x % 2 == 0 => println!("It's an even number: {}", x),
    Some(x) => println!("It's odd: {}", x),
    None => (),
}
Copied!