Rust Pattern Matching: A Practical Guide

Introduction In Rust, pattern matching is a powerful language feature that allows us to perform different operations based on different patterns. Pattern matching can be used in various scenarios, such as working with enum types, destructuring tuples and structs, handling conditional expressions, and more. This article provides a detailed introduction to Rust’s pattern matching syntax and demonstrates its usage and advantages through example code. Basic Usage Rust uses the match keyword for pattern matching. A match expression consists of multiple arms, each containing a pattern and a block of code to execute when the pattern matches. Rust evaluates the arms in order and executes the block corresponding to the first matching pattern. Here is a simple example: fn main() { let number = 3; match number { 1 => println!("One"), 2 => println!("Two"), 3 => println!("Three"), _ => println!("Other"), } } In the code above, we define a variable number and assign it the value 3. Then we use a match expression to match against number. First, Rust checks the first arm with pattern 1; since number is not 1, that arm is skipped. It proceeds to the second arm, 2, which also doesn't match. Finally, it checks the third arm, 3, which matches, so it executes the block and prints Three. If none of the patterns match, the final underscore _ serves as a default case, similar to default in other languages, and executes the corresponding block. Matching Enum Types In Rust, enums are a type that allows you to define a value that can be one of several different variants. Pattern matching is one of the most common ways to handle enums, allowing us to execute different logic depending on the variant. Consider the following example, where we define an enum called Message with three different variants: Move, Write, and ChangeColor: enum Message { Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } Now, we use a match expression to handle the different variants of Message: fn process_message(msg: Message) { match msg { Message::Move { x, y } => println!("Move to coordinates (x={}, y={})", x, y), Message::Write(text) => println!("Write: {}", text), Message::ChangeColor(r, g, b) => println!("Change color to (r={}, g={}, b={})", r, g, b), } } fn main() { let msg1 = Message::Move { x: 10, y: 20 }; let msg2 = Message::Write(String::from("Hello, world!")); let msg3 = Message::ChangeColor(255, 0, 0); process_message(msg1); process_message(msg2); process_message(msg3); } In the code above, we define a function process_message that takes a Message enum as its parameter. Within the match expression, we handle different enum variants with different logic. For the Message::Move variant, we destructure the pattern to get x and y, then print the coordinates. For Message::Write, we print the string directly. For Message::ChangeColor, we destructure r, g, and b, and print the RGB values. In the main function, we create three different Message values and pass them to process_message for handling. Based on the variant, we execute different logic. Destructuring and Matching Structs Besides enums, Rust also supports destructuring and matching structs. A struct is a custom data type composed of multiple fields. We can use patterns to destructure structs and execute operations based on field values. Consider the following example, where we define a struct named Point to represent a point in a 2D space: struct Point { x: i32, y: i32, } Now we use a match expression to destructure and match different Point structs: fn process_point(point: Point) { match point { Point { x, y } => println!("Point coordinates: x={}, y={}", x, y), } } fn main() { let p1 = Point { x: 10, y: 20 }; let p2 = Point { x: -5, y: 15 }; process_point(p1); process_point(p2); } In the code above, we define a function process_point that takes a Point as a parameter. In the match expression, we use the pattern Point { x, y } to destructure the struct’s fields and print them. In the main function, we create two different Point instances and pass them to process_point. Pattern matching makes it easy to access and operate on struct fields. Simplifying Matching with if let In some cases, we only care whether a particular pattern matches and don’t need to handle other patterns. In such cases, the if let expression can simplify the matching process. Consider the following example where we define an enum called Value with two variants: Number and Text: enum Value { Number(i32), Text(String), } Now we use if let to check whether a Value instance is a Number: fn main() { let value = Value::Number(42); if let Value::Number(n) = value { println!("The value is a number: {}", n); } else

Apr 23, 2025 - 23:28
 0
Rust Pattern Matching: A Practical Guide

Cover

Introduction

In Rust, pattern matching is a powerful language feature that allows us to perform different operations based on different patterns. Pattern matching can be used in various scenarios, such as working with enum types, destructuring tuples and structs, handling conditional expressions, and more. This article provides a detailed introduction to Rust’s pattern matching syntax and demonstrates its usage and advantages through example code.

Basic Usage

Rust uses the match keyword for pattern matching. A match expression consists of multiple arms, each containing a pattern and a block of code to execute when the pattern matches. Rust evaluates the arms in order and executes the block corresponding to the first matching pattern. Here is a simple example:

fn main() {
    let number = 3;

    match number {
        1 => println!("One"),
        2 => println!("Two"),
        3 => println!("Three"),
        _ => println!("Other"),
    }
}

In the code above, we define a variable number and assign it the value 3. Then we use a match expression to match against number. First, Rust checks the first arm with pattern 1; since number is not 1, that arm is skipped. It proceeds to the second arm, 2, which also doesn't match. Finally, it checks the third arm, 3, which matches, so it executes the block and prints Three.

If none of the patterns match, the final underscore _ serves as a default case, similar to default in other languages, and executes the corresponding block.

Matching Enum Types

In Rust, enums are a type that allows you to define a value that can be one of several different variants. Pattern matching is one of the most common ways to handle enums, allowing us to execute different logic depending on the variant.

Consider the following example, where we define an enum called Message with three different variants: Move, Write, and ChangeColor:

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

Now, we use a match expression to handle the different variants of Message:

fn process_message(msg: Message) {
    match msg {
        Message::Move { x, y } => println!("Move to coordinates (x={}, y={})", x, y),
        Message::Write(text) => println!("Write: {}", text),
        Message::ChangeColor(r, g, b) => println!("Change color to (r={}, g={}, b={})", r, g, b),
    }
}

fn main() {
    let msg1 = Message::Move { x: 10, y: 20 };
    let msg2 = Message::Write(String::from("Hello, world!"));
    let msg3 = Message::ChangeColor(255, 0, 0);

    process_message(msg1);
    process_message(msg2);
    process_message(msg3);
}

In the code above, we define a function process_message that takes a Message enum as its parameter. Within the match expression, we handle different enum variants with different logic. For the Message::Move variant, we destructure the pattern to get x and y, then print the coordinates. For Message::Write, we print the string directly. For Message::ChangeColor, we destructure r, g, and b, and print the RGB values.

In the main function, we create three different Message values and pass them to process_message for handling. Based on the variant, we execute different logic.

Destructuring and Matching Structs

Besides enums, Rust also supports destructuring and matching structs. A struct is a custom data type composed of multiple fields. We can use patterns to destructure structs and execute operations based on field values.

Consider the following example, where we define a struct named Point to represent a point in a 2D space:

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

Now we use a match expression to destructure and match different Point structs:

fn process_point(point: Point) {
    match point {
        Point { x, y } => println!("Point coordinates: x={}, y={}", x, y),
    }
}

fn main() {
    let p1 = Point { x: 10, y: 20 };
    let p2 = Point { x: -5, y: 15 };

    process_point(p1);
    process_point(p2);
}

In the code above, we define a function process_point that takes a Point as a parameter. In the match expression, we use the pattern Point { x, y } to destructure the struct’s fields and print them.

In the main function, we create two different Point instances and pass them to process_point. Pattern matching makes it easy to access and operate on struct fields.

Simplifying Matching with if let

In some cases, we only care whether a particular pattern matches and don’t need to handle other patterns. In such cases, the if let expression can simplify the matching process.

Consider the following example where we define an enum called Value with two variants: Number and Text:

enum Value {
    Number(i32),
    Text(String),
}

Now we use if let to check whether a Value instance is a Number:

fn main() {
    let value = Value::Number(42);

    if let Value::Number(n) = value {
        println!("The value is a number: {}", n);
    } else {
        println!("The value is not a number");
    }
}

In the code above, we define a Value variable and assign it Value::Number(42). Then we use if let to check whether it’s the Number variant. If so, we destructure and print the number; otherwise, we print a message saying it’s not a number.

Using if let can make code more concise and readable, especially when only one pattern is of interest.

Matching Multiple Patterns

Sometimes, we want to match multiple patterns and execute the same block of code. Rust provides the | operator to match multiple patterns in a single arm.

Consider the following example, where we define a variable number and match multiple patterns:

fn main() {
    let number = 42;

    match number {
        0 | 1 => println!("Zero or one"),
        2 | 3 | 4 => println!("Two, three, or four"),
        _ => println!("Other"),
    }
}

In the code above, we use match to evaluate number. The first arm matches both 0 and 1 using 0 | 1. The second arm matches 2, 3, and 4 using 2 | 3 | 4. The final underscore _ acts as the default case for all other values.

The | operator helps match multiple values cleanly and avoids code duplication.

if let and while let

In addition to match, Rust provides if let and while let for conditional pattern matching.

The if let expression performs matching and executes a block if the condition is true. If it doesn't match, nothing happens.

The while let expression works like if let but repeats the process in a loop as long as the pattern matches.

Here is an example demonstrating both:

fn main() {
    let values = vec![Some(1), Some(2), None, Some(3)];

    for value in values {
        if let Some(num) = value {
            println!("Number: {}", num);
        } else {
            println!("None");
        }
    }

    let mut values = vec![Some(1), Some(2), None, Some(3)];
    while let Some(value) = values.pop() {
        if let Some(num) = value {
            println!("Number: {}", num);
        } else {
            println!("None");
        }
    }
}

In the code above, we first define a vector of Option values. Using a for loop and if let, we check whether each element is Some and print the value, or print "None".

Next, we define another vector and use while let to pop elements one by one. As long as an element is Some, we print its value; otherwise, we print "None".

Using if let and while let, we can flexibly match conditions and handle patterns.

Exhaustiveness Checking in match

In Rust, match expressions are exhaustive. This means the compiler checks whether all possible cases have been handled in a match expression to ensure nothing is missed.

If a match expression is not exhaustive, the compiler issues a warning to help prevent potential errors. To ensure exhaustiveness, we can add a _ arm at the end of the match as a fallback case.

Here is an example that demonstrates exhaustiveness checking:

enum Color {
    Red,
    Green,
    Blue,
}

fn main() {
    let color = Color::Red;

    match color {
        Color::Red => println!("Red"),
        Color::Green => println!("Green"),
        // Missing the Color::Blue branch
    }
}

In the code above, we define an enum Color with three variants. Then we use a match expression to match on the color variable. We provide arms for Color::Red and Color::Green, but omit Color::Blue.

When we try to compile this code, the Rust compiler produces the following warning:

warning: non-exhaustive patterns: `Color::Blue` not covered

This warning indicates that our match expression is not exhaustive because it doesn’t handle all possible cases.

To fix this issue, we can either add a _ branch or explicitly match all enum variants.

Conclusion

Pattern matching is a powerful and flexible language feature in Rust that enables different operations to be performed based on different patterns.

This article introduced the basic usage of pattern matching in Rust, including matching enums and structs, using if let and while let to simplify matching logic, and ensuring exhaustiveness in match expressions to handle all possible cases.

We are Leapcell, your top choice for hosting Rust projects.

Leapcell

Leapcell is the Next-Gen Serverless Platform for Web Hosting, Async Tasks, and Redis:

Multi-Language Support

  • Develop with Node.js, Python, Go, or Rust.

Deploy unlimited projects for free

  • pay only for usage — no requests, no charges.

Unbeatable Cost Efficiency

  • Pay-as-you-go with no idle charges.
  • Example: $25 supports 6.94M requests at a 60ms average response time.

Streamlined Developer Experience

  • Intuitive UI for effortless setup.
  • Fully automated CI/CD pipelines and GitOps integration.
  • Real-time metrics and logging for actionable insights.

Effortless Scalability and High Performance

  • Auto-scaling to handle high concurrency with ease.
  • Zero operational overhead — just focus on building.

Explore more in the Documentation!

Try Leapcell

Follow us on X: @LeapcellHQ

Read on our blog