match
控制流结构
Rust 有一个极其强大的控制流结构叫做 match
,它允许你将一个值与一系列模式进行比较,然后根据哪个模式匹配来执行代码。模式可以由字面值、变量名、通配符和许多其他东西组成;第 18 章涵盖了所有不同类型的模式以及它们的作用。match
的强大之处在于模式的表达能力,以及编译器确认所有可能的情况都得到处理的事实。
将 match
表达式想象成一个硬币分类机:硬币沿着轨道滑下,轨道上有一些大小不一的孔,每枚硬币都会落入它遇到的第一个适合它的孔中。同样,值会经过 match
中的每个模式,并且在值“适合”的第一个模式处,该值会落入相关的代码块中以在执行期间使用。
说到硬币,让我们以硬币为例使用 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
。首先我们列出 match
关键字,后跟一个表达式,在本例中是值 coin
。这看起来与 if
使用的条件表达式非常相似,但有一个很大的区别:对于 if
,条件需要计算为布尔值,但这里它可以是任何类型。本例中 coin
的类型是我们在第一行定义的 Coin
枚举。
接下来是 match
分支。一个分支有两个部分:一个模式和一些代码。这里的第一个分支有一个模式,它是值 Coin::Penny
,然后是 =>
运算符,它分隔模式和要运行的代码。在这种情况下,代码只是值 1
。每个分支之间用逗号分隔。
当 match
表达式执行时,它按顺序将结果值与每个分支的模式进行比较。如果模式与值匹配,则执行与该模式关联的代码。如果该模式与值不匹配,则执行继续到下一个分支,就像在硬币分类机中一样。我们可以根据需要拥有任意数量的分支:在清单 6-3 中,我们的 match
有四个分支。
与每个分支关联的代码是一个表达式,匹配分支中表达式的结果值是整个 match
表达式返回的值。
如果 match 分支代码很短,我们通常不使用花括号,就像清单 6-3 中每个分支只返回一个值一样。如果要在 match 分支中运行多行代码,则必须使用花括号,并且分支后的逗号是可选的。例如,以下代码每次使用 Coin::Penny
调用该方法时都会打印 “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 分支的另一个有用的特性是它们可以绑定到与模式匹配的值的部分。这就是我们可以从枚举变体中提取值的方式。
作为一个例子,让我们更改我们的一个枚举变体以在其中保存数据。从 1999 年到 2008 年,美国铸造了 50 个州中每个州都有不同设计的 25 美分硬币。没有其他硬币获得州设计,因此只有 25 美分硬币具有此额外价值。我们可以通过更改 Quarter
变体以包含存储在其中的 UsState
值来将此信息添加到我们的 enum
中,我们已在清单 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 美分硬币相关的州的名称,这样如果那是我们的朋友没有的州,他们就可以将其添加到他们的收藏中。
在此代码的 match 表达式中,我们向匹配 Coin::Quarter
变体值的模式添加了一个名为 state
的变量。当 Coin::Quarter
匹配时,state
变量将绑定到该 25 美分硬币州的值。然后我们可以在该分支的代码中使用 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)
。当我们将该值与每个 match 分支进行比较时,在到达 Coin::Quarter(state)
之前,没有一个分支匹配。那时,state
的绑定将是值 UsState::Alaska
。然后我们可以在 println!
表达式中使用该绑定,从而从 Quarter
的 Coin
枚举变体中获取内部州值。
使用 Option<T>
进行匹配
在上一节中,我们希望在使用 Option<T>
时从 Some
情况中获取内部 T
值;我们也可以使用 match
处理 Option<T>
,就像我们对 Coin
枚举所做的那样!我们将比较 Option<T>
的变体,而不是比较硬币,但 match
表达式的工作方式保持不变。
假设我们要编写一个函数,该函数接受一个 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)
。然后我们将其与每个 match 分支进行比较
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 分支中的代码,因此我们将 1 加到 i
的值,并创建一个新的 Some
值,其中包含我们的总数 6
。
现在让我们考虑清单 6-5 中 plus_one
的第二次调用,其中 x
是 None
。我们输入 match
并与第一个分支比较
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
和枚举在许多情况下都很有用。你会在 Rust 代码中经常看到这种模式:针对枚举进行 match
,将变量绑定到内部数据,然后根据它执行代码。起初有点棘手,但是一旦你习惯了它,你就会希望在所有语言中都有它。它始终是用户最喜欢的。
Match 是穷尽的
我们需要讨论 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 中的 Match 是穷尽的:为了使代码有效,我们必须穷尽每一种可能性。尤其是在 Option<T>
的情况下,当 Rust 阻止我们忘记显式处理 None
情况时,它可以保护我们免于假设我们有一个值,而我们可能为空,从而使前面讨论的数十亿美元的错误成为不可能。
捕获所有模式和 _
占位符
使用枚举,我们还可以为一些特定值采取特殊操作,但对于所有其他值采取一个默认操作。想象一下我们正在实现一个游戏,如果你掷骰子掷出 3,你的玩家不会移动,而是得到一顶新的漂亮帽子。如果你掷出 7,你的玩家会失去一顶漂亮帽子。对于所有其他值,你的玩家在游戏板上移动该数量的空格。这是一个 match
,它实现了该逻辑,骰子掷出的结果是硬编码的而不是随机值,所有其他逻辑都由没有函数体的函数表示,因为实际实现它们超出了本例的范围
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
函数来使用该变量。
这段代码可以编译,即使我们没有列出 u8
可以拥有的所有可能值,因为最后一个模式将匹配所有未明确列出的值。这个捕获所有模式满足了 match
必须是穷尽的要求。请注意,我们必须将捕获所有分支放在最后,因为模式是按顺序评估的。如果我们将捕获所有分支放在前面,则其他分支将永远不会运行,因此如果我们在一个捕获所有分支之后添加分支,Rust 会警告我们!
Rust 还有一个模式,当我们需要一个捕获所有模式但不希望使用捕获所有模式中的值时可以使用:_
是一个特殊模式,它匹配任何值并且不绑定到该值。这告诉 Rust 我们不打算使用该值,因此 Rust 不会警告我们存在未使用的变量。
让我们更改游戏的规则:现在,如果你掷出除 3 或 7 以外的任何东西,你必须重新掷骰子。我们不再需要使用捕获所有值,因此我们可以更改我们的代码以使用 _
而不是名为 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 值(我们在“元组类型”部分中提到的空元组类型)作为与 _
分支关联的代码来表达这一点
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 我们不打算使用任何其他与早期分支中的模式不匹配的值,并且在这种情况下我们不想运行任何代码。
关于模式和匹配的更多内容,我们将在第 19 章中介绍.
Match 如何与所有权交互
如果枚举包含不可复制的数据(如 String),那么你应该小心 match 是否会移动或借用该数据。例如,这个使用 Option<String>
的程序将编译
但是,如果我们用变量名(例如 Some(s)
)替换 Some(_)
中的占位符,那么程序将不会编译
opt
是一个普通的枚举 — 其类型是 Option<String>
,而不是像 &Option<String>
这样的引用。因此,对 opt
的 match 将移动非忽略字段(如 s
)。请注意,与第一个程序相比,第二个程序中 opt
更早地失去了读取和所有权权限。在 match 表达式之后,opt
中的数据已被移动,因此在 println
中读取 opt
是非法的。
如果我们想查看 opt
而不移动其内容,惯用的解决方案是对引用进行 match
Rust 会将引用从外部枚举 &Option<String>
“下推”到内部字段 &String
。因此,s
的类型为 &String
,并且在 match 之后可以使用 opt
。为了更好地理解这种 “下推” 机制,请参阅 Rust 参考中关于 绑定模式 的部分。