键盘快捷键

按 ← 或 → 在章节之间导航

按 S 或 / 在书中搜索

按 ? 显示此帮助

按 Esc 隐藏此帮助

所有可使用模式的位置

模式在 Rust 的许多地方都会出现,而且你已经在不知不觉中 大量使用了它们!本节讨论所有可以使用模式的合法位置。

match 分支

正如第 6 章所讨论的,我们在 match 表达式的分支中使用模式。 正式地说,match 表达式由关键字 match、一个要匹配的 值,以及一个或多个匹配分支组成,每个分支包含一个模式 和当值与该分支的模式匹配时要运行的表达式,类似这样:

match VALUE {
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
}

例如,这是来自示例 6-5 的 match 表达式,它对变量 x 中的 Option<i32> 值进行匹配:

match x {
    None => None,
    Some(i) => Some(i + 1),
}

这个 match 表达式中的模式就是每个箭头左边的 None 和 Some(i)。

match 表达式的一个要求是它们必须是穷尽的(exhaustive), 即 match 表达式中值的所有可能性都必须被考虑到。 确保你已覆盖每一种可能性的方法之一是在最后一个分支 使用一个通配模式:例如,一个匹配任何值的变量名永远不会 失败,因此可以覆盖所有剩余的情况。

特定的模式 _ 会匹配任何东西,但它永远不会绑定到 变量,因此它经常被用在最后一个 match 分支中。例如,当你想 忽略任何未指定的值时,_ 模式就很有用。我们将在本章 后面的「忽略 模式中的值」一节中更详细地介绍 _ 模式。

let 语句

在本章之前,我们只显式讨论过在 match 和 if let 中使用 模式,但实际上,我们在其他地方也使用了模式, 包括在 let 语句中。例如,考虑下面这个用 let 进行 简单变量赋值的代码:

#![allow(unused)]
fn main() {
let x = 5;
}

每次你使用这样的 let 语句时,你都在使用模式, 尽管你可能没有意识到!更正式地说,let 语句的形式如下:

let PATTERN = EXPRESSION;

在像 let x = 5; 这样的语句中,变量名处于 PATTERN 位置, 此时变量名只是模式的一种特别简单的形式。Rust 将 表达式与模式进行比较,并绑定它找到的任何名称。所以,在 let x = 5; 示例中,x 是一个模式,意思是「将在这里匹配的 内容绑定到变量 x」。因为名称 x 是整个模式, 所以这个模式实际上意味着「将一切内容绑定到变量 x, 无论值是什么」。

为了更清楚地看到 let 的模式匹配层面,考虑示例 19-1,它使用 let 配合模式来解构元组。

fn main() {
    let (x, y, z) = (1, 2, 3);
}
示例 19-1:使用模式解构元组并同时创建三个变量

在这里,我们用元组去匹配一个模式。Rust 将值 (1, 2, 3) 与模式 (x, y, z) 进行比较,发现值与模式匹配——也就是说, 它发现两边的元素数量相同——于是 Rust 把 1 绑定到 x、2 绑定到 y、3 绑定到 z。你可以把这个元组模式 理解为在它内部嵌套了三个独立的变量模式。

如果模式中的元素数量与元组中的元素数量不匹配, 整体类型就会不匹配,我们会得到一个编译器错误。例如, 示例 19-2 展示了试图把一个三元素的元组解构到两个变量中, 这是行不通的。

fn main() {
    let (x, y) = (1, 2, 3);
}
示例 19-2:错误地构造一个模式,其变量与元组中的元素数量不匹配

尝试编译这段代码会产生如下类型错误:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0308]: mismatched types
 --> src/main.rs:2:9
  |
2 |     let (x, y) = (1, 2, 3);
  |         ^^^^^^   --------- this expression has type `({integer}, {integer}, {integer})`
  |         |
  |         expected a tuple with 3 elements, found one with 2 elements
  |
  = note: expected tuple `({integer}, {integer}, {integer})`
             found tuple `(_, _)`

For more information about this error, try `rustc --explain E0308`.
error: could not compile `patterns` (bin "patterns") due to 1 previous error

要修复这个错误,我们可以使用 _ 或 .. 来忽略元组中的 一个或多个值,正如你将在「忽略 模式中的值」一节中看到的那样。如果问题 在于模式中的变量太多,解决方案是移除变量, 使变量数量等于元组中的元素数量,从而让类型匹配。

条件 if let 表达式

在第 6 章中,我们讨论了如何使用 if let 表达式,主要作为编写 只匹配一种情况的 match 等价物的简短方式。可选地, if let 可以有一个对应的 else,包含当 if let 中的模式 不匹配时要运行的代码。

示例 19-3 展示了也可以混用 if let、else if 和 else if let 表达式。 这样做比 match 表达式给了我们更多的灵活性, 因为在 match 表达式中我们只能表示一个值与模式进行比较。此外, Rust 不要求一系列 if let、else if 和 else if let 分支中的 条件彼此相关。

示例 19-3 中的代码基于一系列针对若干条件的检查来决定 使用什么颜色作为背景。在这个示例中,我们创建了 带有硬编码值的变量,而真实程序可能会从用户输入中接收 这些值。

Filename: src/main.rs
fn main() {
    let favorite_color: Option<&str> = None;
    let is_tuesday = false;
    let age: Result<u8, _> = "34".parse();

    if let Some(color) = favorite_color {
        println!("Using your favorite color, {color}, as the background");
    } else if is_tuesday {
        println!("Tuesday is green day!");
    } else if let Ok(age) = age {
        if age > 30 {
            println!("Using purple as the background color");
        } else {
            println!("Using orange as the background color");
        }
    } else {
        println!("Using blue as the background color");
    }
}
示例 19-3:混用 if let、else if、else if let 和 else

如果用户指定了一个最喜欢的颜色,那就使用该颜色作为背景。 如果没有指定最喜欢的颜色且今天是星期二,背景颜色就是 绿色。否则,如果用户以字符串形式指定了他们的年龄, 并且我们能成功地将它解析为数字,那么颜色将根据数字的 值是紫色或橙色。如果这些条件都不适用,背景颜色就是 蓝色。

这种条件结构让我们能够支持复杂的需求。用我们这里的 硬编码值,这个示例将打印 Using purple as the background color。

你可以看到 if let 也可以引入遮蔽现有变量的新变量, 方式与 match 分支相同:if let Ok(age) = age 这一行引入了 一个包含 Ok 变体内值的新 age 变量,遮蔽了现有的 age 变量。这意味着我们需要把 if age > 30 条件放在 那个代码块内:我们无法把这两个条件组合成 if let Ok(age) = age && age > 30。 我们想与 30 比较的新 age 直到新作用域以花括号开始后才有效。

使用 if let 表达式的缺点是编译器不检查 穷尽性,而 match 表达式会检查。如果我们省略了最后的 else 代码块,从而遗漏了某些情况的处理,编译器就 不会提醒我们存在可能的逻辑 bug。

while let 条件循环

while let 条件循环与 if let 结构类似,它允许 只要模式持续匹配,while 循环就持续运行。在示例 19-4 中,我们展示了一个 while let 循环,它等待在线程之间发送的 消息,但在本例中检查的是 Result 而不是 Option。

fn main() {
    let (tx, rx) = std::sync::mpsc::channel();
    std::thread::spawn(move || {
        for val in [1, 2, 3] {
            tx.send(val).unwrap();
        }
    });

    while let Ok(value) = rx.recv() {
        println!("{value}");
    }
}
示例 19-4:使用 while let 循环,只要 rx.recv() 返回 Ok 就持续打印值

这个示例依次打印 1、2,然后 3。recv 方法从通道的 接收端取出第一条消息并返回一个 Ok(value)。当我们 在第 16 章第一次看到 recv 时,我们直接对错误 unwrap, 或者用 for 循环把它当作迭代器使用。然而,正如示例 19-4 所示, 我们还可以使用 while let,因为只要发送方存在, recv 方法在每条消息到达时都会返回一个 Ok, 一旦发送方断开连接就会产生一个 Err。

for 循环

在 for 循环中,紧跟在关键字 for 之后的值是一个 模式。例如,在 for x in y 中,x 就是模式。示例 19-5 演示了如何在 for 循环中使用模式来解构(即拆开) 一个元组,作为 for 循环的一部分。

fn main() {
    let v = vec!['a', 'b', 'c'];

    for (index, value) in v.iter().enumerate() {
        println!("{value} is at index {index}");
    }
}
示例 19-5:在 for 循环中使用模式解构元组

示例 19-5 中的代码将打印以下内容:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.52s
     Running `target/debug/patterns`
a is at index 0
b is at index 1
c is at index 2

我们使用 enumerate 方法对一个迭代器进行改造,使其产生一个值 以及该值的索引,并将它们放入一个元组。产生的第一个值是 元组 (0, 'a')。当这个值与模式 (index, value) 匹配时, index 将是 0 而 value 将是 'a',打印出输出的第一行。

函数参数

函数参数也可以是模式。示例 19-6 中的代码声明了一个 名为 foo 的函数,它接收一个类型为 i32、名为 x 的参数, 现在这段代码应该看起来很熟悉了。

fn foo(x: i32) {
    // code goes here
}

fn main() {}
示例 19-6:在参数中使用模式的函数签名

x 部分就是一个模式!正如我们对 let 所做的那样, 我们可以在函数的参数中把一个元组与模式匹配。示例 19-7 在 我们把元组传给函数时将其中的值拆分。

Filename: src/main.rs
fn print_coordinates(&(x, y): &(i32, i32)) {
    println!("Current location: ({x}, {y})");
}

fn main() {
    let point = (3, 5);
    print_coordinates(&point);
}
示例 19-7:参数解构元组的函数

这段代码会打印 Current location: (3, 5)。值 &(3, 5) 与 模式 &(x, y) 匹配,所以 x 是值 3,y 是值 5。

我们也可以像函数参数列表一样,在闭包参数列表中使用模式, 因为闭包与函数类似,正如在第 13 章所讨论的那样。

到这里,你已经看到了使用模式的几种方式,但模式并不是 在所有可以使用它们的地方都以相同的方式工作。在某些地方, 模式必须是无可反驳的(irrefutable);在其他情况下,它们可以是 可反驳的(refutable)。我们接下来将讨论这两个概念。