Rust enums and pattern matching

Table of contents

1. Definition of enumeration

1.1 Option enumeration and its advantages over null values

 2. match control flow structure

2.1 Match Option

2.2 Matching is exhaustive

2.3 Wildcard pattern and _ placeholder

3. if let concise control flow


1. Definition of enumeration

Enumerations (enumerations), also known as enums. Enumerations allow you to define by enumerating the possible members (variants) a type. First, we'll define and use an enumeration to show how it encodes information along with data. Next, we'll explore a particularly useful enum called Option, which represents a value that is either something or nothing. Then we will talk about using pattern matching in match expressions to write corresponding code to be executed for different enumeration values. Finally, we will introduce if let, another concise and convenient structure for processing enumerations in code.

Let’s look at the following example:

#[derive(Debug)]
enum Sex {
    Man,
    Woman,
}
fn main() {
    let var = Sex::Man;
    println!("value is {:?}", var)
}

From the above code example, we can enumerate gender. When referring to a certain value of the enumeration type, you can refer to a certain attribute value in the enumeration by adding a bunch of colons after the enumeration name.

In the following example, we can use an enumeration whose members can have multiple types:

enum op {
    name(String),
    time(i32),
    People { name: String, age: i32 },
}

Enumerations with associated values ​​work much like defining multiple structures of different types, except that the enumeration does not use the struct keyword and all its members are combined together.

There is another similarity between structures and enumerations: like you can use impl for Just like defining methods on a structure, you can also define methods on an enumeration.

    enum Op {
        Name(String),
        Time(i32),
        People { name: String, age: i32 },
    }

    impl Op {
        fn say(&self) {}
    }

Let's look at another very common and useful enum from the standard library:Option.

1.1 Option enumeration and its advantages over null values

This part will analyze a case of Option , Option is another enumeration defined by the standard library. The Option type is widely used because it encodes a very common scenario, where a value either has value or has no value.

For example, if you request the first item of a non-empty list, you will get a value; if you request an empty list, you will get nothing. Expressing this concept in terms of a type system means that the compiler needs to check that all cases it should handle are handled, so that bugs that are very common in other programming languages ​​can be avoided.

Programming languages ​​are often designed to consider what features are included, but it is also important to consider what features are excluded. Rust doesn't have many null value features that other languages ​​have. Null value (Null ) is a value, which represents no value. In languages ​​with null values, variables always have one of two states: null and non-null.

However, the concept that null is trying to convey is still meaningful: a null value is a value that is currently invalid or missing for some reason.

The problem is not the concept but the specific implementation. For this reason, Rust does not have a null value, but it does have an enumeration that can encode the concept of presence or absence. This enumeration is Option<T>, and it is defined in the standard library as follows:


fn main() {
    enum Option<T> {
        None,
        Some(T),
    }
}

Option<T> is still a regular enumeration, and Some(T) and None are still members of Option<T> . <T> Syntax is a Rust feature we haven’t covered yet. It is a generic type parameter, so all you need to know is that <T> means Option that the Some members of the enumeration can contain any type of data, and the specific type used for each T position makes Option<T> the whole as a different type. Here are some examples containing numeric and string Option values:

 enum Option<T> {
        None,
        Some(T),
    }
 let some_number = Some(5000);
 let some_char = Some('e');
 let some_boolean = Some(true);

Let us look at the following example again. What will happen if we define the following two values ​​to be added?

fn main() {
    enum Option<T> {
        None,
        Some(T),
    }
    let some_number: i8 = 5;

    let absent_number: Option<i8> = Some(5);

    let plus = some_number + absent_number;
}

The running results are as follows:

There are 2 serious problems here:

The first problem is let absent_number: Option<i8> = Some(5); An error will be reported when assigning a value here. These two type names look similar, but they are actually different types and cannot be assigned.

The second one is when adding different types. When you have a value of type like i8 in Rust, the compiler ensures that it always has a valid value. . We can use it confidently without doing null checking. Only when using Option<i8> (or whatever type is used) do we need to worry about the possibility of not having a value, and the compiler will make sure that we handle the null case before using the value.

 2. match control flow structure

Rust has an extremely powerful control flow operator called match which allows us to compare a value with a series of patterns and execute the corresponding code based on the matching pattern. . Patterns can be composed of literals, variables, wildcards, and many other things;

Let's take a look at the following example to understand the role of match more clearly:

fn main() {
    enum Coin {
        Penny,
        Nickel,
        Dime,
        Quarter,
    }

    fn value_in_cents(coin: Coin) -> u8 {
        match coin {
            Coin::Penny => 1,
            Coin::Nickel => 5,
            Coin::Dime => 10,
            Coin::Quarter => 25,
        }
    }
    let res = value_in_cents(Coin::Nickel);
    print!("result {}", res)  // result 5
}

The function of match is similar to switch in other languages ​​(for example, JavaScript). In the above code, the method receives an enumeration type, and match returns different values ​​according to different members of the enumeration type, similar to different branches. Only branches that meet the conditions will be returned last. If a certain branch is matched and you want to execute other logic, you can add a pair of curly braces and write the corresponding logic inside.

    fn value_in_cents(coin: Coin) -> u8 {
        match coin {
            Coin::Penny => {
                print!("res: 执行到这了");
                1
            }
            Coin::Nickel => 5,
            Coin::Dime => 10,
            Coin::Quarter => 25,
        }
    }

2.1 Match Option<T>

Write a function below that gets a Option<i32> and if it contains a value, add one to it. If there is no value in it, the function should return the None value.

fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
    println!("{:?}  {:?}   {:?}", five, six, none)  // Some(5)  Some(6)   None

2.2 Matching is exhaustive

match There is another aspect to discuss: these branches must cover all possibilities. Otherwise, compilation cannot be performed.

fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            Some(i) => Some(i + 1),
        }
    }

According to the above error message, we know that match matching in Rust must be exhaustive, otherwise the compilation will not pass.

2.3 Wildcard pattern and _ placeholder

Let's look at the following example:

fn main() {
    let dice_roll = 9;
    match dice_roll {
        3 => 3,
        7 => 7,
        hello => 9,
    };
}

3 and 7 will match the corresponding values. If you define a variable such as hello, it can match the value in any other situation.

Even if we don't list all possible values ​​of u8 , this code will still compile because the last pattern will match all values ​​that are not specifically listed. This wildcard pattern satisfies the requirement that match must be exhausted. Note that we have to put the wildcard branch last because patterns are matched sequentially. Rust will warn us if we add another branch after the wildcard branch, because branches after that will never be matched.

Rust also provides a mode. When we don’t want to use the value obtained by wildcard mode, please use , which is a special Pattern, which can match any value without binding to it. This tells Rust that we won't use this value, so Rust won't warn us about the unused variable.  _ 

fn main() {
    let dice_roll = 9;
    match dice_roll {
        3 => 3,
        7 => 7,
        _ => 9,
    };
}

When we match something else, we don't want to run any code in this case. An empty tuple can be returned as follows:

fn main() {
    let dice_roll = 9;

    match dice_roll {
        3 => three(),
        7 => seven(),
        _ => (),
    }
    fn three() {}
    fn seven() {}
}

3. if let concise control flow

Let's look at an example first:

fn main() {
    let config_max = Some(3u8);
    match config_max {
        Some(max) => println!("The maximum is configured to be {}", max),
        _ => (),
    }
}

If the value is Some, we want to print out the value in the Some member, which is bound to < in the pattern a i=3> in the variable. We don't want to do anything with the  value. In order to meet the requirements of  expression (exhaustiveness),  must be added after processing this only member, which also adds a lot of annoying boilerplate code. . maxNonematch_ => ()

To simplify the code, you can use if let to simplify it:

fn main() {
    let config_max = Some(3u8);
    if let Some(max) = config_max {
        println!("res {}", max)
    }
}

Using if let means writing less code, less indentation, and less boilerplate code. However, this would lose the exhaustiveness checks mandated by match . The choice between match and if let depends on the specific environment and the trade-off between increased brevity and loss of exhaustive checking.

In other words, if let can be thought of as a syntactic sugar for match , which executes code when the value matches a certain pattern and ignores all other values. .

As for the matching pattern of the lower loop, it can be achieved through if let else, as shown below:

fn main() {
    let mut count = 0;
    let config_max = Some(3u8);
    if let Some(max) = config_max {
        println!("res {}", max)
    } else {
        count += 1;
    }
}

Guess you like

Origin blog.csdn.net/u014388408/article/details/134441999