DEV Community

Cover image for [Rust Guide] 18.1. Where Patterns Can Be Used
SomeB1oody
SomeB1oody

Posted on

[Rust Guide] 18.1. Where Patterns Can Be Used

18.1.1 What Is a Pattern?

A pattern is a special piece of Rust syntax used to match the structure of complex and simple types.

Using patterns together with match expressions and other constructs gives you better control over program flow.

Patterns are made up of some combination of the following elements:

  • literals
  • destructured arrays, enums, structs, and tuples
  • variables
  • wildcards
  • placeholders

To use a pattern, you compare it with a value: if the pattern matches, you can use the corresponding part of the value in the code.

18.1.2 match Arms

An arm can use a pattern. Its form is:

match VALUE {
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
}
Enter fullscreen mode Exit fullscreen mode

match is required to be exhaustive, meaning you must account for all possible cases.

The _ wildcard is also commonly used in match. It matches anything and does not bind to a variable. It is usually used in the last match arm or when you want to ignore a value.

For a more detailed introduction, see 6.3. The Match Control Flow Operator.

18.1.3 if let Expressions

An if let expression can be thought of as a match expression that matches only one possibility.

It can optionally include:

  • else if
  • else
  • else if let

The disadvantage of if let compared with match is that it does not check exhaustiveness. If we omit the final else block and therefore miss some cases, the compiler will not warn us about a possible logic error. Take a look:

fn main() {
    let favorite_color: Option<&str> = None;
    let is_tuesday = false;
    let age: Result<u8, _> = "34".parse();

    if let Some(color) = favorite_color {
        println!("Using your favorite color, {color}, as the background");
    } else if is_tuesday {
        println!("Tuesday is green day!");
    } else if let Ok(age) = age {
        if age > 30 {
            println!("Using purple as the background color");
        } else {
            println!("Using orange as the background color");
        }
    } else {
        println!("Using blue as the background color");
    }
}
Enter fullscreen mode Exit fullscreen mode

If the user specifies a favorite color, that color is used as the background. If no favorite color is specified and today is Tuesday, then the background color is green. Otherwise, if the user specifies their age as a string and we can successfully parse it as a number, then the color is purple or orange depending on the numeric value. If none of those conditions apply, the background color is blue.

This kind of conditional structure lets us support complex requirements. With the hard-coded values here, this example prints Using purple as the background color.

You can see that if let can also introduce shadowing variables in the same way as match: the line if let Ok(age) = age introduces a new shadowing age variable containing the value inside Ok. That means we need to place if age > 30 inside that block: we cannot combine the two conditions into if let Ok(age) = age && age > 30. The shadowing age we want to compare with 30 is only valid inside the new scope that begins with the { braces.

See 6.4. Simple Control Flow - If Let for the rest of the details.

18.1.4 while let Conditional Loop

while let is somewhat similar to if let; as long as the pattern continues to match, it allows the while loop to keep running.

Take a look at this example:

let mut stack = Vec::new();

stack.push(1);
stack.push(2);
stack.push(3);

while let Some(top) = stack.pop() {
    println!("{top}");
}
Enter fullscreen mode Exit fullscreen mode

This example prints 3, then 2, then 1. The pop method removes the last element from the vector and returns Some(value). If the vector is empty, pop returns None. As long as pop returns Some, the while loop continues running the code in its block. When pop returns None, the loop stops. We can use while let to pop each element from the stack.

18.1.5 for Loops

for loops are the most common loops in Rust. In a for loop, the pattern is the value that comes immediately after the for keyword.

In a for loop, the value that directly follows the keyword for is a pattern. For example, in for x in y, x is the pattern. The following example demonstrates how to use a pattern in a for loop to destructure a tuple as part of the loop:

let v = vec!['a', 'b', 'c'];

for (index, value) in v.iter().enumerate() {
    println!("{value} is at index {index}");
}
Enter fullscreen mode Exit fullscreen mode

Output:

a is at index 0
b is at index 1
c is at index 2
Enter fullscreen mode Exit fullscreen mode

See 3.6. Control Flow: Loops for the rest of the information.

18.1.6 let Statements

let statements are also patterns, and their syntax is:

let PATTERN = EXPRESSION;
Enter fullscreen mode Exit fullscreen mode

Take a look at this example:

let (x, y, z) = (1, 2, 3);
Enter fullscreen mode Exit fullscreen mode

We match the tuple against the pattern. Rust compares the value (1, 2, 3) to the pattern (x, y, z) and sees that the value matches the pattern, so Rust binds 1 to x, 2 to y, and 3 to z. You can think of this tuple pattern as three separate variable patterns nested inside it.

18.1.7 Function Parameters

Function parameters can also be patterns. Take a look:

fn foo(x: i32) {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

The x part is a pattern.

As with let, we can match a tuple in a function parameter against a pattern. For example:

fn print_coordinates(&(x, y): &(i32, i32)) {
    println!("Current location: ({x}, {y})");
}

fn main() {
    let point = (3, 5);
    print_coordinates(&point);
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)