RustBrock/Pattern Syntax.md
darkicewolf50 730b0091e8
All checks were successful
Test Gitea Actions / first (push) Successful in 21s
Test Gitea Actions / check-code (push) Successful in 18s
Test Gitea Actions / test (push) Successful in 18s
Test Gitea Actions / documentation-check (push) Successful in 17s
finished ch19.2 and started ch19.3
2025-04-09 17:00:42 -06:00

9.5 KiB

Pattern Syntax

Here we gather all the syntax valid in patterns and discuss why and when you might want to use each one.

Matching Literals

As you saw previously in Ch6, you can match patterns against literals directly.

Here is an example of this

    let x = 1;

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

The code prints one because the value in x is 1.

This syntax is useful when you want your code to take an action if it gets a particular concrete value.

Matching Named Variables

Named variables are irrefutable patterns that match any value, and we have used them many times.

However, there is a complication when you use named variables in match, if let, or while let expressions.

Because each kinds of expression starts a new scope, variables declared as part of a pattern inside the expression will shadow those with the same name outside, as is the case with all variables.

Here we declare a variable named x with the value Some(5) and a variable y with the value 10.

Next we create a match expression on the value x.

Look at the patterns in the match arms and println! at the end, and try to figure out what the code will print before running this code or reading further.

    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}");

Lets run through what happens when the match expression runs.

The pattern in the first match arm doesn't match the defined value of x, so the code continues.

The pattern in the second match arm introduces a new variable named y that will match any value inside a Some value.

Because we are in a new scope inside the match expression, this is a new y variable, not the y we declared at the beginning with the value 10.

This new y binding will match any value inside a Some, which is what we have in x.

Therefore the new y binds to the inner value of the Some in x.

That value is 5 so the expression for that arm executes and prints Matched , y = 5.

If x has been a None value instead of Some(5), the patterns in the first two arms wouldn't have matched. so the value would have matched to the underscore.

We didn't introduce the x variable in the pattern of the underscore arm, so the x in the expression is still the outer x that hasn't been shadowed.

For this hypothetical case, the maatch would pint Default case, x = None.

When the match expression is done, its scope ends, and so does the scope of the inner y.

The last println! produces at the end: x = Some(5), y = 10.

To create a match expression that compares the values of the outer x and y rather than introducing a new variable which shadows the exiting y variable.

We would need to use a match guard conditional instead.

This will be covered later in the ch.

Multiple Patterns

You can match multiple patterns using the | syntax, which is the pattern or operator.

For example in the following code we match the value of x against the match arms, the first of which has an or option, meaning if the value of x matches either of the values in that arm, that arm's code will run

    let x = 1;

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

This code prints one or two.

Matching Ranges of Values with ..=

The ..= syntax allows us to match to an inclusive range of values.

Here when a pattern matches any of the values within the given range, that arm will execute

    let x = 5;

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

If x is 1, 2, 3, 4, or 5, the first arm will match.

This syntax is more convenient for multiple match values that using the | operator to express the same idea.

If we were to use | we would need to do 1 | 2 | 3 | 4 | 5.

Specifying a range is much shorter, especially if we want to match, say any number between 1 and 1,000.

The compiler checks that the range isn't empty at compile time, because only types for which Rust can tell if a range is empty or not are char and numeric values, ranges are only allowed with numeric or car values.

Rust can tell that 'c' is within the first pattern's range and prints early ASCII letter.

Destructuring to Break Apart Values

We can also use patterns to destructure structs, enums, and tuples to use different parts of these values.

We will walk through each value.

Destructuring Structs

This code shows a Point struct with two fields, x and y, that we can break apart using a patter with a let statement

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);
}

This creates the variables a and b that match the values of the x and y fields of the p struct.

This shows that the names of the variables in the pattern don't have to match the field names of the struct.

However it is common to match the variable names to the field names to make it easier to remember which variables came from which fields.

Because of this common usage, and because writing let Point { x: x, y: y} = p; contains a lot of duplication, Rust has a shorthand for patterns that match struct fields.

You only need to list the name of the struct field, and the variables created from the pattern will have the same names.

This code behaves in the same way as om the code before, but the variables created in the let pattern are x and y instead of a and b.

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);
}

This code creates the variables x and y that match the x and y fields of the p variable.

The outcome is that the variables x and y contain the values form the p struct.

We can also destructure with literal values as part of the struct pattern rather than creating variables for all the fields.

Doing this allows us to test some of the fields for particular values while creating variables to destructure the other fields.

Here we have a match expression that separates Point values into three cases.

Points that lie directly on the x axis (which is true when y = 0)

On the y axis (x = 0)

Or Neither

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})");
        }
    }
}

The first arm will match any point that lies on the x axis by specifying that the y field matches if its value matches the literal 0.

The pattern still creates an x variable that we can use in the code for this arm.

The second arm matches any point on the y axis by specifying that the x field matches if its value is 0 and creates a variable y for the value of the y field.

The third arm doesn't specify any literals, so it matches any other Point and creates variables that we can use in the code for this arm.

Here the value p matches the second arm by virtue of x containing a 0, so this code will print On the x axis at 0.

Destructuring Enums

We have destructured enums before, but haven't explicitly discussed that the pattern to destructure an enum corresponds to the way the data stored within the enum is defined.

For example, here we use the Message enum from Ch6 and write a match with patterns that will destructure each inner value.

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}");
        }
    }
}

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

Try changing the value of msg to see the code form the other arms run.

For enum variants without any data, like Message::Quit.

We can't destructure the value any further.

We can only match on the literal Message::Quit value and no variables are in that pattern.

For struct-like enum variants, like Message::Move.

We can use a pattern similar to the pattern we specify to match structs.

After the variant name, we place curly brackets and then list the fields with variable so we break apart the pieces to use in the code for this arm.

We did use the shorthand form as we did before.

For tuple-like enum variants, like Message::Write that holds a tuple with one element and Message::ChangeColor that holds a tuple with three elements.

The pattern is similar to the pattern we specify to match tuples.

The number of variables in the pattern must match the number of elements in the variant we are matching.

Destructuring Nested Structs and Enums