match 控制流结构
The match Control Flow Construct
Rust 有一个极其强大的控制流结构,叫做 match,它允许你将一个值与一系列模式进行比较,并根据哪个模式匹配来执行代码。模式可以由字面量值、变量名、通配符和许多其他东西组成;第 19 章涵盖了所有不同种类的模式及其作用。match 的强大之处在于模式的表达能力,以及编译器会确认所有可能的情况都已得到处理。
Rust has an extremely powerful control flow construct called match that
allows you to compare a value against a series of patterns and then execute
code based on which pattern matches. Patterns can be made up of literal values,
variable names, wildcards, and many other things; Chapter
19 covers all the different kinds of patterns
and what they do. The power of match comes from the expressiveness of the
patterns and the fact that the compiler confirms that all possible cases are
handled.
你可以把 match 表达式想象成一台硬币分拣机:硬币滑落下一条轨道,轨道上散布着大小不一的孔,每枚硬币都会掉进它遇到的第一个适合它的孔里。同样地,值会经过 match 中的每个模式,在值“适合”的第一个模式处,该值会掉进相关的代码块中,以便在执行期间使用。
Think of a match expression as being like a coin-sorting machine: Coins slide
down a track with variously sized holes along it, and each coin falls through
the first hole it encounters that it fits into. In the same way, values go
through each pattern in a match, and at the first pattern the value “fits,”
the value falls into the associated code block to be used during execution.
说到硬币,让我们用它们作为使用 match 的例子!我们可以编写一个函数,它接收一个未知的美国硬币,并以与分拣机类似的方式,确定它是哪种硬币并返回其以美分为单位的值,如示例 6-3 所示。
Speaking of coins, let’s use them as an example using match! We can write a
function that takes an unknown US coin and, in a similar way as the counting
machine, determines which coin it is and returns its value in cents, as shown
in Listing 6-3.
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,
}
}
fn main() {}
让我们分解 value_in_cents 函数中的 match。首先,我们列出 match 关键字,后跟一个表达式,在本例中是值 coin。这看起来与 if 使用的条件表达式非常相似,但有一个很大的区别:对于 if,条件需要计算为布尔值,但在这里它可以是任何类型。本例中 coin 的类型是我们第一行定义的 Coin 枚举。
Let’s break down the match in the value_in_cents function. First, we list
the match keyword followed by an expression, which in this case is the value
coin. This seems very similar to a conditional expression used with if, but
there’s a big difference: With if, the condition needs to evaluate to a
Boolean value, but here it can be any type. The type of coin in this example
is the Coin enum that we defined on the first line.
接下来是 match 的“分支”(arms)。一个分支由两部分组成:一个模式和一些代码。这里的第一个分支有一个模式,它是值 Coin::Penny,然后是 => 运算符,用于分隔模式和要运行的代码。在本例中,代码只是值 1。每个分支与下一个分支之间用逗号分隔。
Next are the match arms. An arm has two parts: a pattern and some code. The
first arm here has a pattern that is the value Coin::Penny and then the =>
operator that separates the pattern and the code to run. The code in this case
is just the value 1. Each arm is separated from the next with a comma.
当 match 表达式执行时,它会按顺序将结果值与每个分支的模式进行比较。如果一个模式匹配该值,则执行与该模式关联的代码。如果该模式不匹配该值,执行将继续到下一个分支,就像在硬币分拣机中一样。我们可以根据需要拥有任意数量的分支:在示例 6-3 中,我们的 match 有四个分支。
When the match expression executes, it compares the resultant value against
the pattern of each arm, in order. If a pattern matches the value, the code
associated with that pattern is executed. If that pattern doesn’t match the
value, execution continues to the next arm, much as in a coin-sorting machine.
We can have as many arms as we need: In Listing 6-3, our match has four arms.
与每个分支关联的代码是一个表达式,而匹配分支中表达式的结果值就是整个 match 表达式返回的值。
The code associated with each arm is an expression, and the resultant value of
the expression in the matching arm is the value that gets returned for the
entire match expression.
如果匹配分支的代码很短,我们通常不使用花括号,就像示例 6-3 中每个分支只返回一个值那样。如果你想在匹配分支中运行多行代码,必须使用花括号,此时分支后面的逗号是可选的。例如,以下代码在每次使用 Coin::Penny 调用该方法时都会打印 “Lucky penny!”,但它仍然返回代码块的最后一个值 1:
We don’t typically use curly brackets if the match arm code is short, as it is
in Listing 6-3 where each arm just returns a value. If you want to run multiple
lines of code in a match arm, you must use curly brackets, and the comma
following the arm is then optional. For example, the following code prints
“Lucky penny!” every time the method is called with a Coin::Penny, but it
still returns the last value of the block, 1:
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => {
println!("Lucky penny!");
1
}
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
fn main() {}
绑定到值的模式
Patterns That Bind to Values
匹配分支的另一个有用特性是,它们可以绑定到匹配模式的值的部分。这就是我们如何从枚举变体中提取值的方法。
Another useful feature of match arms is that they can bind to the parts of the values that match the pattern. This is how we can extract values out of enum variants.
作为一个例子,让我们更改一个枚举变体以在其内部持有数据。从 1999 年到 2008 年,美国铸造了 25 美分硬币(quarter),其一面针对 50 个州分别采用了不同的设计。其他硬币都没有州设计,因此只有 25 美分硬币具有此额外值。我们可以通过更改 Quarter 变体以包含存储在其内部的 UsState 值来将此信息添加到我们的 enum 中,我们在示例 6-4 中已经这样做了。
As an example, let’s change one of our enum variants to hold data inside it.
From 1999 through 2008, the United States minted quarters with different
designs for each of the 50 states on one side. No other coins got state
designs, so only quarters have this extra value. We can add this information to
our enum by changing the Quarter variant to include a UsState value
stored inside it, which we’ve done in Listing 6-4.
#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
Alabama,
Alaska,
// --snip--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn main() {}
让我们设想一个朋友正试图收集所有 50 个州的 25 美分硬币。当我们按硬币类型分拣零钱时,我们还会喊出与每枚 25 美分硬币相关的州的名称,这样如果是我们朋友没有的那个州,他们就可以将其添加到他们的收藏中。
Let’s imagine that a friend is trying to collect all 50 state quarters. While we sort our loose change by coin type, we’ll also call out the name of the state associated with each quarter so that if it’s one our friend doesn’t have, they can add it to their collection.
在此代码的匹配表达式中,我们在匹配变体 Coin::Quarter 值的模式中添加了一个名为 state 的变量。当匹配到 Coin::Quarter 时,state 变量将绑定到该 25 美分硬币所属州的值。然后,我们可以在该分支的代码中使用 state,如下所示:
In the match expression for this code, we add a variable called state to the
pattern that matches values of the variant Coin::Quarter. When a
Coin::Quarter matches, the state variable will bind to the value of that
quarter’s state. Then, we can use state in the code for that arm, like so:
#[derive(Debug)]
enum UsState {
Alabama,
Alaska,
// --snip--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {state:?}!");
25
}
}
}
fn main() {
value_in_cents(Coin::Quarter(UsState::Alaska));
}
如果我们调用 value_in_cents(Coin::Quarter(UsState::Alaska)),coin 将是 Coin::Quarter(UsState::Alaska)。当我们将该值与每个匹配分支进行比较时,在到达 Coin::Quarter(state) 之前,它们都不匹配。届时,state 的绑定将是值 UsState::Alaska。然后我们可以在 println! 表达式中使用该绑定,从而从 Quarter 的 Coin 枚举变体中提取出内部的州值。
If we were to call value_in_cents(Coin::Quarter(UsState::Alaska)), coin
would be Coin::Quarter(UsState::Alaska). When we compare that value with each
of the match arms, none of them match until we reach Coin::Quarter(state). At
that point, the binding for state will be the value UsState::Alaska. We can
then use that binding in the println! expression, thus getting the inner
state value out of the Coin enum variant for Quarter.
匹配 Option<T>
The Option<T> match Pattern
在上一节中,我们想在使用 Option<T> 时从 Some 情况中提取出内部的 T 值;我们也可以像处理 Coin 枚举一样,使用 match 处理 Option<T>!我们将比较 Option<T> 的变体,而不是比较硬币,但 match 表达式的工作方式保持不变。
In the previous section, we wanted to get the inner T value out of the Some
case when using Option<T>; we can also handle Option<T> using match, as
we did with the Coin enum! Instead of comparing coins, we’ll compare the
variants of Option<T>, but the way the match expression works remains the
same.
假设我们要编写一个函数,它接收一个 Option<i32>,如果有值在内部,就给该值加 1。如果内部没有值,函数应返回 None 值,并且不尝试执行任何操作。
Let’s say we want to write a function that takes an Option<i32> and, if
there’s a value inside, adds 1 to that value. If there isn’t a value inside,
the function should return the None value and not attempt to perform any
operations.
由于有了 match,这个函数非常容易编写,看起来就像示例 6-5。
This function is very easy to write, thanks to match, and will look like
Listing 6-5.
fn main() {
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);
}
让我们更详细地研究一下 plus_one 的第一次执行。当我们调用 plus_one(five) 时,plus_one 体内的变量 x 将具有值 Some(5)。然后我们将其与每个匹配分支进行比较:
Let’s examine the first execution of plus_one in more detail. When we call
plus_one(five), the variable x in the body of plus_one will have the
value Some(5). We then compare that against each match arm:
fn main() {
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);
}
Some(5) 值不匹配模式 None,所以我们继续下一个分支:
fn main() {
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);
}
The Some(5) value doesn’t match the pattern None, so we continue to the
next arm:
fn main() {
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);
}
Some(5) 匹配 Some(i) 吗?是的!我们有相同的变体。i 绑定到 Some 中包含的值,所以 i 取值 5。然后执行匹配分支中的代码,所以我们给 i 的值加 1,并创建一个内部总和为 6 的新 Some 值。
Does Some(5) match Some(i)? It does! We have the same variant. The i
binds to the value contained in Some, so i takes the value 5. The code in
the match arm is then executed, so we add 1 to the value of i and create a
new Some value with our total 6 inside.
现在让我们考虑示例 6-5 中 plus_one 的第二次调用,其中 x 是 None。我们进入 match 并与第一个分支比较:
Now let’s consider the second call of plus_one in Listing 6-5, where x is
None. We enter the match and compare to the first arm:
fn main() {
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);
}
匹配成功!没有值可以相加,所以程序停止并返回 => 右侧的 None 值。因为第一个分支匹配了,所以不会再比较其他分支。
It matches! There’s no value to add to, so the program stops and returns the
None value on the right side of =>. Because the first arm matched, no other
arms are compared.
将 match 和枚举结合使用在许多情况下都很有用。你会在 Rust 代码中经常看到这种模式:针对枚举进行 match,将一个变量绑定到内部数据,然后根据其执行代码。起初这有点棘手,但一旦习惯了,你就会希望所有语言都有这个功能。它始终是用户的最爱。
Combining match and enums is useful in many situations. You’ll see this
pattern a lot in Rust code: match against an enum, bind a variable to the
data inside, and then execute code based on it. It’s a bit tricky at first, but
once you get used to it, you’ll wish you had it in all languages. It’s
consistently a user favorite.
匹配是穷尽的
Matches Are Exhaustive
我们还需要讨论 match 的另一个方面:分支的模式必须涵盖所有可能性。考虑一下我们 plus_one 函数的这个版本,它有一个 bug 且无法编译:
There’s one other aspect of match we need to discuss: The arms’ patterns must
cover all possibilities. Consider this version of our plus_one function,
which has a bug and won’t compile:
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
我们没有处理 None 的情况,所以这段代码会导致 bug。幸运的是,这是 Rust 知道如何捕捉的 bug。如果我们尝试编译这段代码,我们会得到这个错误:
$ cargo run
Compiling enums v0.1.0 (file:///projects/enums)
error[E0004]: non-exhaustive patterns: `None` not covered
--> src/main.rs:3:15
|
3 | match x {
| ^ pattern `None` not covered
|
note: `Option<i32>` defined here
--> /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/option.rs:593:1
::: /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/option.rs:597:5
|
= note: not covered
= note: the matched value is of type `Option<i32>`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
|
4 ~ Some(i) => Some(i + 1),
5 ~ None => todo!(),
|
For more information about this error, try `rustc --explain E0004`.
error: could not compile `enums` (bin "enums") due to 1 previous error
Rust 知道我们没有涵盖所有可能的情况,甚至知道我们忘记了哪个模式!Rust 中的匹配是“穷尽的”(exhaustive):为了使代码有效,我们必须穷尽最后一种可能性。特别是在 Option<T> 的情况下,当 Rust 阻止我们忘记显式处理 None 情况时,它保护我们免于假设拥有一个值(而实际上可能是空值),从而使前面讨论的价值十亿美元的错误变得不可能。
Rust knows that we didn’t cover every possible case and even knows which
pattern we forgot! Matches in Rust are exhaustive: We must exhaust every last
possibility in order for the code to be valid. Especially in the case of
Option<T>, when Rust prevents us from forgetting to explicitly handle the
None case, it protects us from assuming that we have a value when we might
have null, thus making the billion-dollar mistake discussed earlier impossible.
通配模式和 _ 占位符
Catch-All Patterns and the _ Placeholder
使用枚举,我们还可以对几个特定值采取特殊操作,但对所有其他值采取一个默认操作。想象一下,我们正在实现一个游戏,如果你掷骰子掷到 3,你的角色不会移动,而是会得到一顶漂亮的新帽子。如果你掷到 7,你的角色会失去一顶漂亮的帽子。对于所有其他值,你的角色会在游戏板上移动相应格数。这里有一个实现该逻辑的 match,骰子的结果是硬编码的而不是随机值,所有其他逻辑由没有函数体的函数表示,因为实际实现它们超出了本例的范围:
Using enums, we can also take special actions for a few particular values, but
for all other values take one default action. Imagine we’re implementing a game
where, if you roll a 3 on a dice roll, your player doesn’t move but instead
gets a fancy new hat. If you roll a 7, your player loses a fancy hat. For all
other values, your player moves that number of spaces on the game board. Here’s
a match that implements that logic, with the result of the dice roll
hardcoded rather than a random value, and all other logic represented by
functions without bodies because actually implementing them is out of scope for
this example:
fn main() {
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
other => move_player(other),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}
}
对于前两个分支,模式是字面量值 3 和 7。对于涵盖所有其他可能值的最后一个分支,模式是我们选择命名为 other 的变量。为 other 分支运行的代码通过将其传递给 move_player 函数来使用该变量。
For the first two arms, the patterns are the literal values 3 and 7. For
the last arm that covers every other possible value, the pattern is the
variable we’ve chosen to name other. The code that runs for the other arm
uses the variable by passing it to the move_player function.
即使我们没有列出 u8 可能具有的所有值,这段代码也可以编译,因为最后一个模式将匹配所有未明确列出的值。这个通配模式满足了 match 必须是穷尽的要求。注意我们必须把通配分支放在最后,因为模式是按顺序评估的。如果我们把通配分支放在前面,其他分支将永远不会运行,所以如果你在通配分支后面添加分支,Rust 会警告你!
This code compiles, even though we haven’t listed all the possible values a
u8 can have, because the last pattern will match all values not specifically
listed. This catch-all pattern meets the requirement that match must be
exhaustive. Note that we have to put the catch-all arm last because the
patterns are evaluated in order. If we had put the catch-all arm earlier, the
other arms would never run, so Rust will warn us if we add arms after a
catch-all!
当我们想要一个通配模式但又不想“使用”通配模式中的值时,Rust 还有一个模式可以使用:_ 是一个特殊的模式,它匹配任何值且不绑定到该值。这告诉 Rust 我们不打算使用该值,所以 Rust 不会警告我们变量未使用。
Rust also has a pattern we can use when we want a catch-all but don’t want to
use the value in the catch-all pattern: _ is a special pattern that matches
any value and does not bind to that value. This tells Rust we aren’t going to
use the value, so Rust won’t warn us about an unused variable.
让我们改变游戏规则:现在,如果你掷出 3 或 7 以外的任何数字,你必须重新掷一次。我们不再需要使用通配值,所以我们可以将代码改为使用 _ 而不是名为 other 的变量:
Let’s change the rules of the game: Now, if you roll anything other than a 3 or
a 7, you must roll again. We no longer need to use the catch-all value, so we
can change our code to use _ instead of the variable named other:
fn main() {
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => reroll(),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn reroll() {}
}
这个例子也满足了穷尽性要求,因为我们在最后一个分支中显式地忽略了所有其他值;我们没有遗漏任何东西。
This example also meets the exhaustiveness requirement because we’re explicitly ignoring all other values in the last arm; we haven’t forgotten anything.
最后,我们再次更改游戏规则,如果你掷出 3 或 7 以外的任何数字,你的回合什么都不会发生。我们可以通过使用单元值(我们在“元组类型”部分提到的空元组类型)作为 _ 分支对应的代码来表达这一点:
Finally, we’ll change the rules of the game one more time so that nothing else
happens on your turn if you roll anything other than a 3 or a 7. We can express
that by using the unit value (the empty tuple type we mentioned in “The Tuple
Type” section) as the code that goes with the _ arm:
fn main() {
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => (),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
}
在这里,我们显式地告诉 Rust 我们不会使用任何不匹配前面分支模式的其他值,并且在这种情况下我们不想运行任何代码。
Here, we’re telling Rust explicitly that we aren’t going to use any other value that doesn’t match a pattern in an earlier arm, and we don’t want to run any code in this case.
关于模式和匹配的内容,我们将在第 19 章中进一步讨论。现在,我们要继续讨论 if let 语法,它在 match 表达式显得有点冗长的情况下非常有用。
There’s more about patterns and matching that we’ll cover in Chapter
19. For now, we’re going to move on to the
if let syntax, which can be useful in situations where the match expression
is a bit wordy.