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!表达式中使用该绑定,从而从 QuarterCoin 枚举变体中获取内部状态值。


选项<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) {}
}


对于前两个分支,模式是文本值 37。对于覆盖所有其他可能值的最后一个分支,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 表达式有点冗长的情况下非常有用。