match 控制流结构

Rust 拥有一个极其强大的控制流结构,名为 match,它允许你将一个值与一系列模式进行比较,然后根据哪个模式匹配来执行代码。模式可以由字面值、变量名、通配符和许多其他东西组成;第 19 章 涵盖了所有不同类型的模式及其作用。match 的强大之处在于模式的表达性以及编译器确认所有可能的情况都得到处理这一事实。涵盖了所有不同类型的模式及其作用。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 个州中每个州的不同设计。没有其他硬币获得州设计,因此只有四分之一美元具有此额外价值。我们可以通过更改 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! 表达式中使用该绑定,从而从 QuarterCoin 枚举变体中获取内部州值。

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 分支中的代码,因此我们将 i 的值加 1,并创建一个新的 Some 值,其中包含我们的总数 6

现在让我们考虑清单 6-5 中 plus_one 的第二次调用,其中 xNone。我们进入 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 函数的这个版本,它有一个错误并且无法编译

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 情况,因此此代码将导致错误。幸运的是,Rust 知道如何捕获这个错误。如果我们尝试编译此代码,我们将收到此错误

$ 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 --> file:///home/.rustup/toolchains/1.82/lib/rustlib/src/rust/library/core/src/option.rs:571:1 | 571 | pub enum Option<T> { | ^^^^^^^^^^^^^^^^^^ ... 575 | None, | ---- 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 情况时,它可以保护我们免受在我们可能遇到 null 时假设我们有一个值的错误,从而使前面讨论的数十亿美元的错误成为不可能。

捕获所有模式和 _ 占位符

使用枚举,我们还可以对一些特定值采取特殊操作,但对所有其他值采取一个默认操作。想象一下,我们正在实现一个游戏,如果你掷骰子掷出 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) {} }

对于前两个分支,模式是字面值 37。对于涵盖所有其他可能值的最后一个分支,模式是我们选择命名为 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,则在你的回合中不会发生其他任何事情。我们可以通过使用单元值(我们在 “元组类型” 中提到的空元组类型部分)作为与 _ 分支一起使用的代码

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 章 中介绍。现在,我们将继续讨论 if let 语法,在 match 表达式有点冗长的情况下,if let 语法可能很有用。