Table of contents
1.1 Option enumeration and its advantages over null values
2. match control flow structure
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. . max
None
match
_ => ()
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;
}
}