模式可以使用的所有地方

模式在 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 表达式中的模式是每个箭头左边的 NoneSome(i)

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

特定的模式 _ 会匹配任何内容,但它不会绑定到变量,因此通常用于最后一个匹配臂。当您想要忽略未指定的任何值时,_ 模式非常有用。我们将在本章后面的 “在模式中忽略值” 部分详细讨论 _ 模式。

条件 if let 表达式

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

列表19-1显示,也可以混合使用if letelse ifelse if let表达式。这样做比只能表达一个值与模式进行比较的match表达式提供了更多的灵活性。此外,Rust不要求一系列if letelse ifelse if let分支中的条件相互关联。

清单 19-1 中的代码根据一系列检查多个条件来确定背景颜色。在这个例子中,我们创建了具有硬编码值的变量,这些值在实际程序中可能来自用户输入。

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");
    }
}
Listing 19-1: Mixing if let, else if, else if let, and else

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

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

你可以看到 if let 也可以像 match 分支一样引入被遮蔽的变量:行 if let Ok(age) = age 引入了一个新的被遮蔽的 age 变量,该变量包含 Ok 变体中的值。这意味着我们需要将 if age > 30 条件放在该块内:我们不能将这两个条件组合成 if let Ok(age) = age && age > 30。我们想要与 30 比较的被遮蔽的 age 直到新作用域开始的大括号才有效。

使用 if let 表达式的缺点是编译器不会检查穷尽性,而使用 match 表达式时编译器会进行检查。如果我们省略了最后一个 else 块,因此没有处理某些情况,编译器不会提醒我们可能存在逻辑错误。

while let 条件循环

if let 的构造类似,while let 条件循环允许 while 循环在模式继续匹配的情况下一直运行。我们在第 17 章首次看到了 while let 循环,当时我们使用它来在流产生新值时持续循环。同样,在清单 19-2 中,我们展示了一个 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}");
    }
}
Listing 19-2: Using a while let loop to print values for as long as rx.recv() returns Ok

这个示例打印 1、2 和 3。当我们在第 16 章看到 recv 时,我们直接解包了错误,或者使用 for 循环作为迭代器与之交互。然而,如列表 19-2 所示,我们也可以使用 while let,因为只要发送方正在生成消息,recv 方法就会返回 Ok,而一旦发送方断开连接,就会生成一个 Err

for 循环

for 循环中,直接跟在 for 关键字后面的值是一个模式。例如,在 for x in y 中,x 是模式。列表 19-3 展示了如何在 for 循环中使用模式来解构,或拆分,一个元组作为 for 循环的一部分。

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

    for (index, value) in v.iter().enumerate() {
        println!("{value} is at index {index}");
    }
}
Listing 19-3: Using a pattern in a for loop to destructure a tuple

清单 19-3 中的代码将打印以下内容:

$ 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 将是 0value 将是 'a',打印输出的第一行。

let 语句

在本章之前,我们只明确讨论了在 matchif 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-4,它使用带有 let 的模式来解构一个元组。

fn main() {
    let (x, y, z) = (1, 2, 3);
}
Listing 19-4: Using a pattern to destructure a tuple and create three variables at once

在这里,我们将一个元组与一个模式进行匹配。Rust 将值 (1, 2, 3) 与模式 (x, y, z) 进行比较,发现值与模式匹配,因此 Rust 将 1 绑定到 x2 绑定到 y3 绑定到 z。你可以将这个元组模式视为嵌套了三个单独的变量模式。

如果模式中的元素数量与元组中的元素数量不匹配,整体类型将不匹配,我们将收到编译器错误。例如,列表 19-5 显示了尝试将包含三个元素的元组解构为两个变量,这将无法工作。

fn main() {
    let (x, y) = (1, 2, 3);
}
Listing 19-5: Incorrectly constructing a pattern whose variables don’t match the number of elements in the tuple

尝试编译此代码会导致此类型错误:

$ 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

要修复错误,我们可以使用 _.. 忽略元组中的一个或多个值,如“在模式中忽略值” 部分所述。如果问题是模式中的变量过多,解决方案是通过删除变量使类型匹配,使变量的数量等于元组中的元素数量。

函数参数

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

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

fn main() {}
Listing 19-6: A function signature uses patterns in the parameters

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);
}
Listing 19-7: A function with parameters that destructure a tuple

这段代码打印 Current location: (3, 5)。值 &(3, 5) 匹配模式 &(x, y),因此 x 的值为 3y 的值为 5

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

在这一点上,您已经看到了使用模式的几种方式,但模式在我们能够使用它们的每个地方并不都以相同的方式工作。在某些地方,模式必须是不可反驳的;在其他情况下,它们可以是可反驳的。我们将在接下来讨论这两个概念。