DEV Community

Cover image for [Rust Guide] 18.3. Pattern Syntax
SomeB1oody
SomeB1oody

Posted on

[Rust Guide] 18.3. Pattern Syntax

If you find this helpful, please like, bookmark, and follow. To keep learning along, follow this series.

18.3.1 Matching Literals

Patterns can match literals directly. Take a look at this example:

let x = 1;

match x {
    1 => println!("one"),
    2 => println!("two"),
    3 => println!("three"),
    _ => println!("anything"),
}
Enter fullscreen mode Exit fullscreen mode

This code prints one because the value in x is 1. This syntax is very useful when you want the code to act on a specific value.

18.3.2 Matching Named Variables

Named variables are irrefutable patterns that can match any value. Take a look:

let x = Some(5);
let y = 10;

match x {
    Some(50) => println!("Got 50"),
    Some(y) => println!("Matched, y = {y}"),
    _ => println!("Default case, x = {x:?}"),
}

println!("at the end: x = {x:?}, y = {y}");
Enter fullscreen mode Exit fullscreen mode

The logic in this example is simple; the key point is that there are two y names here. They are unrelated and live in different scopes. The y in let y = 10 is used to store 10, while the y in Some(y) is used to extract the data carried by the Some variant of the Option type.

The execution logic in match is as follows:

  • The pattern in the first arm does not match the value of x, so execution continues.

  • The pattern in the second arm introduces a new variable named y, which matches any value inside Some. Because we are in a new scope inside the match expression, this is a new y variable, not the y declared at the beginning with value 10. This new y binding matches whatever value is inside Some, and that is what we have in x. Therefore, this new y is bound to the inner value of Some in x. That value is 5, so the expression in that arm executes and prints Matched, y = 5.

  • If x were None instead of Some(5)—which of course cannot happen in this example—then the patterns in the first two arms would not match, so the value would match _. We do not introduce an x variable in the wildcard arm, so the x in the expression is still the outer x that has not been shadowed. In that hypothetical case, match would print Default case, x = None.

Output:

Matched, y = 5
at the end: x = Some(5), y = 10
Enter fullscreen mode Exit fullscreen mode

18.3.3 Multiple Patterns

Inside a match expression, you can use the pipe symbol | syntax, meaning or, to match multiple patterns. Take a look:

let x = 1;

match x {
    1 | 2 => println!("one or two"),
    3 => println!("three"),
    _ => println!("anything"),
}
Enter fullscreen mode Exit fullscreen mode

The first arm in the example matches when x is 1 or 2.

18.3.4 Using ..= to Match a Range of Values

Take a look:

let x = 5;

match x {
    1..=5 => println!("one through five"),
    _ => println!("something else"),
}
Enter fullscreen mode Exit fullscreen mode

The first arm in this example means that when x is any value from 1 to 5 inclusive—namely 1, 2, 3, 4, or 5—it will match.

Because the only types for which Rust can determine whether a range is empty are char and numeric types, ranges are allowed only for numbers or char values. Take a look:

let x = 'c';

match x {
    'a'..='j' => println!("early ASCII letter"),
    'k'..='z' => println!("late ASCII letter"),
    _ => println!("something else"),
}
Enter fullscreen mode Exit fullscreen mode

The first arm in this example matches characters from a to j, and the second arm matches characters from k to z.

18.3.5 Destructuring to Break Values Apart

We can use patterns to destructure structs, enums, and tuples so that we can refer to different parts of values of those types.

Destructuring structs

Take a look:

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

fn main() {
    let p = Point { x: 0, y: 7 };

    let Point { x: a, y: b } = p;
    assert_eq!(0, a);
    assert_eq!(7, b);
}
Enter fullscreen mode Exit fullscreen mode
  • The Point struct has two fields, x and y, both of type i32.
  • There is a Point instance called p, whose x field is 0 and whose y field is 7.
  • Then we destructure p with a pattern, binding the value of x to a and the value of y to b.

This is still a bit verbose. If we change the variable names a to x and b to y, we can shorten it like this:

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

fn main() {
    let p = Point { x: 0, y: 7 };

    let Point { x, y } = p;
    assert_eq!(0, x);
    assert_eq!(7, y);
}
Enter fullscreen mode Exit fullscreen mode

Destructuring can also be used flexibly. Take a look:

fn main() {
    let p = Point { x: 0, y: 7 };

    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})");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
  • The first arm requires the x field to be anything and the y field to be 0.
  • The second arm requires the x field to be 0 and the y field to be anything.
  • The third arm imposes no restrictions on the values of x and y.

Destructuring enums

Take a look:

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 {x} and in the y direction {y}");
        }
        Message::Write(text) => {
            println!("Text message: {text}");
        }
        Message::ChangeColor(r, g, b) => {
            println!("Change the color to red {r}, green {g}, and blue {b}")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This code prints Change the color to red 0, green 160, and blue 255.

Destructuring Nested structs and enums

Take a look:

enum Color {
    Rgb(i32, i32, i32),
    Hsv(i32, i32, i32),
}

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

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

    match msg {
        Message::ChangeColor(Color::Rgb(r, g, b)) => {
            println!("Change color to red {r}, green {g}, and blue {b}");
        }
        Message::ChangeColor(Color::Hsv(h, s, v)) => {
            println!("Change color to hue {h}, saturation {s}, value {v}")
        }
        _ => (),
    }
}
Enter fullscreen mode Exit fullscreen mode

The data carried by the ChangeColor variant of Message is the Color enum. When using a match expression, just match layer by layer. In the first two arms of the match, the outer layer is the ChangeColor variant, and the inner layer corresponds to the two variants of Color; the values inside can all be extracted through variables.

Destructuring structs and Tuples

Take a look:

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

fn main() {
    let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
}
Enter fullscreen mode Exit fullscreen mode

The outer layer of the pattern in main is a tuple with two elements:

  • The first element is itself a tuple with two elements.
  • The second element is a Point struct.

Ignoring Values in Patterns

There are several ways to ignore an entire value or part of a value in a pattern:

  • _: ignore the entire value
  • _ combined with other patterns: ignore part of a value
  • names that start with _
  • ..: ignore the remaining part of a value

Using _ to Ignore an Entire Value

Take a look:

fn foo(_: i32, y: i32) {
    println!("This code only uses the y parameter: {y}");
}

fn main() {
    foo(3, 4);
}
Enter fullscreen mode Exit fullscreen mode

This code completely ignores the value 3 passed as the first parameter and prints This code only uses the y parameter: 4.

Using Nested _ to Ignore Part of a Value

Take a look:

let mut setting_value = Some(5);
let new_setting_value = Some(10);

match (setting_value, new_setting_value) {
    (Some(_), Some(_)) => {
        println!("Can't overwrite an existing customized value");
    }
    _ => {
        setting_value = new_setting_value;
    }
}

println!("setting is {setting_value:?}");
Enter fullscreen mode Exit fullscreen mode

This code prints Can't overwrite an existing customized value and then setting is Some(5). In the first arm, we do not need to match or use the values inside the Some variants, but we do need to determine that setting_value and new_setting_value are Some variants. That is what it means to ignore part of a value.

The second arm means that in all other cases—if setting_value or new_setting_value is None—we assign new_setting_value to setting_value. This is an example of using _ with other patterns to ignore a value.

We can also use underscores in multiple places in one pattern to ignore specific values. Take a look:

let numbers = (2, 4, 8, 16, 32);

match numbers {
    (first, _, third, _, fifth) => {
        println!("Some numbers: {first}, {third}, {fifth}")
    }
}
Enter fullscreen mode Exit fullscreen mode

This ignores the second and fourth elements of the tuple. The code prints Some numbers: 2, 8, 32, and the values 4 and 16 are ignored.

Using Names Starting With _ to Ignore Unused Variables

Take a look:

fn main() {
    let _x = 5;
    let y = 10;
}
Enter fullscreen mode Exit fullscreen mode

Normally, if you create a variable and do not use it, the Rust compiler warns you. x and y are not used here, but there is a warning for y. That is because _x starts with _, which tells the compiler that this is a temporary variable.

Please note that there is a subtle difference between using only _ and using a name that starts with _. The syntax _x still binds the value to a variable, while _ does not bind anything at all. Take a look:

let s = Some(String::from("Hello!"));

if let Some(_s) = s {
    println!("found a string");
}

println!("{s:?}");
Enter fullscreen mode Exit fullscreen mode

We get an error because the value of s is still moved into _s, which prevents us from printing s.

In this situation, you should use _ to avoid binding the value:

let s = Some(String::from("Hello!"));

if let Some(_) = s {
    println!("found a string");
}

println!("{s:?}");
Enter fullscreen mode Exit fullscreen mode

Using .. to Ignore the Rest of a Value

Take a look:

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

fn main() {
    let origin = Point { x: 0, y: 0, z: 0 };

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

When matching with match, we only need the x field, so the pattern writes only x, and the rest is covered by ...

Using .. this way is also fine:

fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (first, .., last) => {
            println!("Some numbers: {first}, {last}");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This takes only the first and last values and ignores the rest.

This use of .. is not allowed:

fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (.., second, ..) => {
            println!("Some numbers: {second}")
        },
    }
}
Enter fullscreen mode Exit fullscreen mode

Here there is .. in both the front and the back, and we want the middle element. But which element exactly? Written this way, the compiler does not know how many elements .. should skip, so it also does not know which element second refers to.

Output:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
error: `..` can only be used once per tuple pattern
 --> src/main.rs:5:22
  |
5 |         (.., second, ..) => {
  |          --          ^^ can only be used once per tuple pattern
  |          |
  |          previously used here

error: could not compile `patterns` (bin "patterns") due to 1 previous error
Enter fullscreen mode Exit fullscreen mode

18.3.6 Using match guards to Add Extra Conditions

match guards are extra if conditions after a match arm pattern. For the arm to match, the condition must also be satisfied. match guards are useful for situations more complex than a plain pattern.

Take a look:

fn main() {
    let num = Some(4);

    match num {
        Some(x) if x % 2 == 0 => println!("The number {x} is even"),
        Some(x) => println!("The number {x} is odd"),
        None => (),
    }
}
Enter fullscreen mode Exit fullscreen mode

In the first arm of the match, Some(x) is the pattern, and if x % 2 == 0 is the match guard, which requires the data carried by Some to be divisible by 2.

The condition if x % 2 == 0 cannot be expressed in the pattern itself, so match guards let us express this logic. The downside of this extra expressiveness is that when a match guard is involved, the compiler will not try to check exhaustiveness.

Look at the second example:

fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Got 50"),
        Some(n) if n == y => println!("Matched, n = {n}"),
        _ => println!("Default case, x = {x:?}"),
    }

    println!("at the end: x = {x:?}, y = {y}");
}
Enter fullscreen mode Exit fullscreen mode

This code now prints Default case, x = Some(5).

The match guard if n == y is not a pattern, so it does not introduce a new variable. This y is the outer y (with value 10), not a new shadowing y. We can use the comparison to find values n that have the same value as the outer y.

Look at the third example:

let x = 4;
let y = false;

match x {
    4 | 5 | 6 if y => println!("yes"),
    _ => println!("no"),
}
Enter fullscreen mode Exit fullscreen mode

This example uses match guards together with multiple patterns.

The matching condition says that this arm matches only when x is 4, 5, or 6 and y is true. When this code runs, x is 4, but the match guard y is false, so the first arm does not execute and the second arm prints no.

The important thing to notice here is the precedence of the pattern relative to the match guard. It should be:

(4 | 5 | 6) if y => ...
Enter fullscreen mode Exit fullscreen mode

not:

4 | 5 | (6 if y) => ...
Enter fullscreen mode Exit fullscreen mode

18.3.7 @ Bindings

The @ symbol lets us create a variable that stores a value while we test whether that value matches a pattern.

Take a look:

enum Message {
    Hello { id: i32 },
}

fn main() {
    let msg = Message::Hello { id: 5 };

    match msg {
        Message::Hello {
            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}"),
    }
}
Enter fullscreen mode Exit fullscreen mode

In the first arm of this match, the value of the id field is bound to id_variable while also being checked to see whether it falls within the inclusive range from 3 to 7.

Top comments (0)