匹配控制流结构

Beginner

This tutorial is from open-source community. Access the source code

简介

欢迎来到匹配控制流结构。本实验是《Rust 程序设计语言》的一部分。你可以在 LabEx 中练习你的 Rust 技能。

在本实验中,我们将探索 Rust 中强大的 match 控制流结构,它允许进行模式匹配并根据匹配的模式执行代码。

match 控制流结构

Rust 有一个极其强大的控制流结构,称为 match,它允许你将一个值与一系列模式进行比较,然后根据匹配的模式执行代码。模式可以由字面值、变量名、通配符等组成;第 18 章涵盖了所有不同类型的模式及其作用。match 的强大之处在于模式的表现力以及编译器会确认所有可能的情况都得到了处理。

可以将 match 表达式想象成一台硬币分类机:硬币沿着一条有各种大小孔洞的轨道下滑,每枚硬币会落入它遇到的第一个能容纳它的孔洞。同样地,值会在 match 中逐个通过每个模式,当值“适合”第一个模式时,它就会落入相关的代码块中,以便在执行期间使用。

说到硬币,让我们以硬币为例来使用 match!我们可以编写一个函数,它接受一枚未知的美国硬币,并以类似于计数机的方式确定它是哪种硬币,并返回其以美分为单位的价值,如清单 6-3 所示。

1 enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
  2 match coin {
      3 Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

清单 6-3:一个枚举和一个 match 表达式,该表达式将枚举的变体用作其模式

让我们来剖析一下 value_in_cents 函数中的 match。首先,我们列出 match 关键字,后面跟着一个表达式,在这个例子中是值 coin [2]。这看起来与用于 if 的表达式非常相似,但有一个很大的区别:对于 if,表达式需要返回一个布尔值,但在这里它可以返回任何类型。在这个例子中,coin 的类型是我们在 [1] 处定义的 Coin 枚举。

接下来是 match 分支。一个分支有两部分:一个模式和一些代码。这里的第一个分支的模式是值 Coin::Penny,然后是分隔模式和要运行的代码的 => 运算符 [3]。在这种情况下,代码只是值 1。每个分支用逗号与下一个分支隔开。

match 表达式执行时,它会按顺序将结果值与每个分支的模式进行比较。如果一个模式与值匹配,与该模式相关联的代码就会被执行。如果该模式与值不匹配,执行会继续到下一个分支,就像在硬币分类机中一样。我们可以根据需要拥有任意数量的分支:在清单 6-3 中,我们的 match 有四个分支。

与每个分支相关联的代码是一个表达式,匹配分支中表达式的结果值就是整个 match 表达式返回的值。

如果 match 分支代码很短,我们通常不使用花括号,就像清单 6-3 中每个分支只返回一个值那样。如果你想在 match 分支中运行多行代码,就必须使用花括号,并且分支后面的逗号是可选的。例如,以下代码在每次使用 Coin::Penny 调用该方法时都会打印“幸运便士!”,但仍然返回块的最后一个值 1

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        }
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

绑定到值的模式

match 分支的另一个有用特性是它们可以绑定到与模式匹配的值的各个部分。这就是我们从枚举变体中提取值的方式。

例如,让我们将枚举变体之一改为在其中包含数据。从 1999 年到 2008 年,美国铸造了 25 美分硬币,其一面为 50 个州中的每个州设计了不同图案。没有其他硬币有州图案,所以只有 25 美分硬币有这个额外的值。我们可以通过将 Quarter 变体改为包含存储在其中的 UsState 值,将此信息添加到我们的 enum 中,我们已经在清单 6-4 中这样做了。

#[derive(Debug)] // 这样我们稍后就能检查状态
enum UsState {
    Alabama,
    Alaska,
    --省略部分--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

清单 6-4:一个 Coin 枚举,其中 Quarter 变体还包含一个 UsState

假设你的一个朋友正在尝试收集所有 50 个州的 25 美分硬币。当我们按硬币类型整理零钱时,我们还会说出与每个 25 美分硬币相关的州名,这样如果是朋友没有的州,他们就可以将其添加到收藏中。

在这段代码的 match 表达式中,我们在与变体 Coin::Quarter 的值匹配的模式中添加了一个名为 state 的变量。当 Coin::Quarter 匹配时,state 变量将绑定到该 25 美分硬币的州的值。然后我们可以在该分支的代码中使用 state,如下所示:

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
        }
    }
}

如果我们调用 value_in_cents(Coin::Quarter(UsState::Alaska))coin 将是 Coin::Quarter(UsState::Alaska)。当我们将该值与每个 match 分支进行比较时,在到达 Coin::Quarter(state) 之前,它们都不匹配。在这一点上,state 的绑定将是值 UsState::Alaska。然后我们可以在 println! 表达式中使用该绑定,从而从 Coin 枚举变体 Quarter 中获取内部的州值。

Option<T> 进行匹配

在上一节中,当使用 Option<T> 时,我们想从 Some 情况中获取内部的 T 值;我们也可以像处理 Coin 枚举那样,使用 match 来处理 Option<T>!这里不是比较硬币,而是比较 Option<T> 的变体,但 match 表达式的工作方式是一样的。

假设我们想编写一个函数,它接受一个 Option<i32>,如果其中有值,就对该值加 1。如果其中没有值,函数应该返回 None 值,并且不尝试执行任何操作。

多亏了 match,这个函数很容易编写,如下所示,如清单 6-5 所示。

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
      1 None => None,
      2 Some(i) => Some(i + 1),
    }
}

let five = Some(5);
let six = plus_one(five); 3
let none = plus_one(None); 4

清单 6-5:一个对 Option<i32> 使用 match 表达式的函数

让我们更详细地研究一下 plus_one 的第一次执行。当我们调用 plus_one(five) [3] 时,plus_one 函数体中的变量 x 将具有值 Some(5)。然后我们将其与每个匹配分支进行比较:

None => None,

Some(5) 值与模式 None 不匹配 [1],所以我们继续到下一个分支:

Some(i) => Some(i + 1),

Some(5)Some(i) 匹配吗 [2]?是的,匹配!我们有相同的变体。i 绑定到 Some 中包含的值,所以 i 取值为 5。然后执行匹配分支中的代码,所以我们将 i 的值加 1,并创建一个新的 Some 值,其中包含我们的总和 6

现在让我们考虑清单 6-5 中 plus_one 的第二次调用,其中 xNone [4]。我们进入 match 并与第一个分支进行比较 [1]。

它匹配!没有值可加,所以程序停止并返回 => 右侧的 None 值。因为第一个分支匹配,所以不会比较其他分支。

在许多情况下,将 match 和枚举结合使用很有用。你会在 Rust 代码中经常看到这种模式:对枚举进行 match,将一个变量绑定到内部的数据,然后根据它执行代码。一开始可能有点棘手,但一旦你习惯了,你会希望所有语言都有它。它一直是用户的最爱。

match 必须穷举所有情况

关于 match,我们还需要讨论另一个方面:分支的模式必须涵盖所有可能性。看看我们的 plus_one 函数的这个版本,它有一个错误并且无法编译:

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        Some(i) => Some(i + 1),
    }
}

我们没有处理 None 的情况,所以这段代码会导致一个错误。幸运的是,这是 Rust 能够捕获的错误。如果我们尝试编译这段代码,会得到如下错误:

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
      = 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!(),
    |

Rust 知道我们没有涵盖每一种可能的情况,甚至知道我们忘记了哪种模式!Rust 中的 match穷举的:我们必须穷尽每一种可能性,代码才有效。特别是在 Option<T> 的情况下,当 Rust 防止我们忘记显式处理 None 情况时,它保护我们不会在可能为 null 的时候假设我们有一个值,从而避免了前面讨论的那种造成巨大损失的错误。

通配模式与 _ 占位符

使用枚举时,我们还可以针对几个特定值采取特殊操作,而对所有其他值采取一个默认操作。想象一下我们正在实现一个游戏,如果你掷骰子掷出 3,你的玩家不会移动,而是会得到一顶新的漂亮帽子。如果你掷出 7,你的玩家会失去一顶漂亮帽子。对于所有其他值,你的玩家会在游戏板上移动相应数量的格子。下面是一个实现该逻辑的 match,其中骰子掷出的结果是硬编码的,而不是随机值,并且所有其他逻辑由没有函数体的函数表示,因为实际实现它们超出了本示例的范围:

let dice_roll = 9;
match dice_roll {
    3 => add_fancy_hat(),
    7 => remove_fancy_hat(),
  1 other => move_player(other),
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}

对于前两个分支,模式是字面值 37。对于涵盖所有其他可能值的最后一个分支,模式是我们选择命名为 other 的变量 [1]。为 other 分支运行的代码通过将变量传递给 move_player 函数来使用该变量。

这段代码可以编译,尽管我们没有列出 u8 可能具有的所有可能值,因为最后一个模式将匹配所有未明确列出的值。这种通配模式满足了 match 必须穷举所有情况的要求。请注意,我们必须将通配分支放在最后,因为模式是按顺序求值的。如果我们将通配分支放在前面,其他分支将永远不会运行,所以如果我们在通配分支之后添加分支,Rust 会警告我们!

当我们想要一个通配模式但又不想在通配模式中使用该值时,Rust 还有一个模式可供我们使用:_ 是一个特殊模式,它匹配任何值,但不会绑定到该值。这告诉 Rust 我们不会使用该值,所以 Rust 不会因未使用变量而警告我们。

让我们更改游戏规则:现在,如果你掷出的不是 3 或 7,你必须再次掷骰子。我们不再需要使用通配值,所以我们可以将代码更改为使用 _ 而不是名为 other 的变量:

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,在你的回合中就不会发生其他任何事情。我们可以通过使用单元值(我们在“元组类型”中提到的空元组类型)作为与 _ 分支相关联的代码来表达这一点:

let dice_roll = 9;
match dice_roll {
    3 => add_fancy_hat(),
    7 => remove_fancy_hat(),
    _ => (),
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}

在这里,我们明确告诉 Rust,我们不会使用任何与前面分支中的模式不匹配的其他值,并且在这种情况下我们不想运行任何代码。

关于模式和匹配,我们将在第 18 章中介绍更多内容。现在,我们将继续介绍 if let 语法,它在 match 表达式有点冗长的情况下可能会很有用。

总结

恭喜你!你已经完成了“match 控制流结构”实验。你可以在 LabEx 中练习更多实验来提升你的技能。