match
控制流构造
Rust 有一个非常强大的控制流结构,称为 match
,它允许你将一个值与一系列模式进行比较,然后根据哪个模式匹配来执行代码。模式可以由 Literal 值、变量名称、通配符和许多其他内容组成;章
18 涵盖了所有不同类型的模式及其作用。match
的强大之处在于模式的表达性,以及编译器确认已处理所有可能的情况这一事实。
将匹配
表达式想象成一台硬币分拣机:硬币沿着带有不同大小孔的轨道滑下,每个硬币都从它遇到的第一个孔中落下。同样,值会通过匹配
中的每个模式,在第一个模式中,值 “fits”,该值落入要在执行期间使用的关联代码块中。
说到硬币,让我们以 match
为例!我们可以编写一个函数,它接受一个未知的美国硬币,并以类似于计数机的方式,确定它是哪个硬币并返回以美分为单位的值,如示例 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() {}
示例 6-3:一个枚举和一个 match
表达式,其模式是枚举的变体
让我们在 value_in_cents
函数中分解匹配项
。首先,我们列出 match
关键字,后跟一个表达式,在本例中为值
硬币
。这似乎与与 if
一起使用的条件表达式非常相似,但有一个很大的区别:使用 if
,条件需要计算为布尔值,但这里它可以是任何类型。此示例中的 coin
类型是我们在第一行定义的 Coin
枚举。
接下来是火柴
臂。arm 有两个部分:一个模式和一些代码。这里的第一个分支有一个模式,值为 Coin::P enny
,然后是 =>
运算符,该运算符将模式和要运行的代码分开。本例中的代码
只是值 1
。每个分支都用逗号分隔。
当 match
表达式执行时,它会按顺序将结果值与每个分支的模式进行比较。如果模式与该值匹配,则执行与该模式关联的代码。如果该模式与值不匹配,则继续执行到下一个分支,就像在硬币分拣机中一样。我们需要多少 arms 就有多少:在示例 6-3 中,我们的 match
有 4 个 arms。
与每个 arm 关联的代码是一个表达式,匹配 arm 中表达式的结果值是为整个 match
表达式返回的值。
如果 match arm 代码很短,我们通常不会使用大括号,就像示例 6-3 中每个 arm 只返回一个值一样。如果要在匹配臂中运行多行代码,则必须使用大括号,并且臂后面的逗号是可选的。例如,以下代码在每次使用 Coin::P enny
调用该方法时都会打印 “Lucky penny!”,但仍然返回区块的最后一个值 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() {}
绑定到值的模式
match arms 的另一个有用功能是它们可以绑定到与模式匹配的值部分。这就是我们从 enum 变体中提取值的方法。
例如,让我们更改其中一个枚举变体以在其中保存数据。从 1999 年到 2008 年,美国为 50 个州中的每一个州铸造了一面设计不同的 25 美分硬币。没有其他硬币有国家设计,所以只有 25 美分硬币有这个额外的价值。我们可以通过更改 Quarter
变体来包含存储在其中的 UsState
值,从而将此信息添加到我们的枚举
中,我们在示例 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() {}
示例 6-4:一个 Coin
枚举,其中 Quarter
变体也包含一个 UsState
值
假设一个朋友正在尝试收集所有 50 个州的 25 美分硬币。当我们按硬币类型对零钱进行排序时,我们还会说出与每个 25 美分硬币相关的州名,这样如果我们的朋友没有,他们可以将其添加到他们的收藏中。
在此代码的匹配表达式中,我们将一个名为 state
的变量添加到匹配变体 Coin::Quarter
的值的模式中。当
Coin::Quarter
匹配时,状态
变量将绑定到该季度的状态值。然后我们可以在该 ARM 的代码中使用 state
,如下所示:
#[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
枚举变体中获取内部状态值。
与选项<T>
匹配
在上一节中,我们想从 Some
中获取内部 T
值
使用选项<T>
时的情况;我们也可以使用 match
来处理 Option<T>
,就像我们对 Coin
枚举所做的那样!我们将比较 Option<T>
的变体,而不是比较硬币,但匹配
表达式的工作方式保持不变。
假设我们想编写一个采用 Option<i32>
的函数,如果里面有一个值,则将该值加 1。如果内部没有值,则函数应返回 None
值,并且不尝试执行任何作。
多亏了 match
,这个函数很容易编写,看起来像示例 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); }
示例 6-5:在 Option<i32>
上使用 match
表达式的函数
让我们更详细地研究一下 plus_one
的第一次执行。当我们调用
plus_one(five)
中,plus_one
主体中的变量 x
将具有值 Some(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);
}
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);
}
Some(5)
是否与 Some(i)
匹配?确实如此!我们有相同的变体。i
绑定到 Some
中包含的值,因此 i
取值 5
。然后执行 match arm 中的代码,因此我们将 i
的值加 1 并创建一个新的 Some
值,其中总共为 6
。
现在让我们考虑示例 6-5 中 plus_one
的第二次调用,其中 x
是
无
。我们进入匹配项
并与第一只手臂进行比较:
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 值。由于第一个分支匹配,因此不会比较其他分支。
在许多情况下,将 match
和 enum 组合在一起很有用。你会在 Rust 代码中经常看到这种模式:匹配
一个枚举,将一个变量绑定到里面的数据,然后基于它执行代码。一开始有点棘手,但一旦你习惯了它,你会希望你拥有所有语言的它。它一直是用户的最爱。
匹配项详尽无遗
我们需要讨论 Match
的另一个方面:手臂的图案必须涵盖所有可能性。考虑一下这个版本的 plus_one
函数,它有一个 bug 并且不会编译:
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/eeb90cda1969383f56a2637cbd3037bdf598841c/library/core/src/option.rs:574:1
::: /rustc/eeb90cda1969383f56a2637cbd3037bdf598841c/library/core/src/option.rs:578: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 中的匹配项是详尽的:我们必须穷尽每一个
的可能性,才能使代码有效。尤其是在
Option<T>
,当 Rust 阻止我们忘记显式处理
none
大小写,它保护我们避免在可能有 null 时假设我们有值,从而使前面讨论的 10 亿美元的错误变得不可能。
Catch-all 模式和 _
占位符
使用枚举,我们还可以为一些特定值采取特殊作,但对于所有其他值,采取一个默认作。想象一下,我们正在实现一个游戏,如果您在掷骰子时掷出 3,您的玩家不会移动,而是获得一顶新的花哨帽子。如果您掷出 7,您的玩家将失去一顶花哨的帽子。对于所有其他值,玩家在游戏板上移动该数量的空格。下面是一个实现该逻辑的匹配项
,其中掷骰子的结果是硬编码的,而不是随机值,所有其他逻辑都由没有主体的函数表示,因为实际实现它们超出了此示例的范围:
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
。对于覆盖所有其他可能值的最后一个分支,pattern 是我们选择命名为 other
的变量。为另一个
分支运行的代码通过将变量传递给 move_player
函数来使用该变量。
这段代码可以编译,即使我们没有列出所有可能的值
u8
可以有,因为最后一个模式将匹配未明确列出的所有值。此 catch-all 模式满足 match
必须详尽的要求。请注意,我们必须将 catch-all 臂放在最后,因为 pattern 是按顺序评估的。如果我们早点放置 catch-all 分支,其他分支将永远不会运行,因此如果我们在 catch-all 之后添加分支,Rust 会警告我们!
Rust 也有一个模式,当我们想要一个 catch-all 但不想时可以使用
使用 catch-all 模式中的值:_
是一种特殊模式,它与任何值匹配,并且不绑定到该值。这告诉 Rust 我们不会使用这个值,所以 Rust 不会警告我们未使用的变量。
让我们改变游戏规则:现在,如果您掷出 3 或 7 以外的任何东西,则必须再次掷骰。我们不再需要使用 catch-all 值,因此我们可以将代码更改为使用 _
而不是名为 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() {} }
此示例还满足穷举性要求,因为我们显式忽略了最后一个分支中的所有其他值;我们没有忘记任何东西。
最后,我们将再次更改游戏规则,这样,如果您掷出 3 或 7 以外的任何内容,则轮到您时不会发生任何其他事情。我们可以通过使用 unit 值(我们在“元组
Type“ 部分)作为 _
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,我们不会使用任何其他与早期 arm 中的模式不匹配的值,并且在这种情况下我们不想运行任何代码。
我们将在 章节 中介绍更多关于 pattern 和 matching 的信息
18. 现在,我们将继续讨论
if let
语法,这在 match
表达式有点冗长的情况下非常有用。