模式可以使用的所有位置

模式在 Rust 中出现在许多地方,而你一直在使用它们,却没有意识到!本节讨论模式有效的所有位置。

match 分支

正如在第 6 章中讨论的那样,我们在 match 表达式的分支中使用模式。形式上,match 表达式被定义为关键字 match,一个要匹配的值,以及一个或多个匹配分支,这些分支由一个模式和一个表达式组成,如果值与该分支的模式匹配,则运行该表达式,就像这样

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

例如,这是 Listing 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 中的模式不匹配时要运行的代码。

Listing 18-1 表明,也可以混合和匹配 if letelse ifelse if let 表达式。这样做比 match 表达式更灵活,在 match 表达式中,我们只能表达一个要与模式比较的值。此外,Rust 不要求一系列 if letelse ifelse if let 分支中的条件相互关联。

Listing 18-1 中的代码根据对几个条件的系列检查来确定背景颜色。对于此示例,我们创建了具有硬编码值的变量,真实程序可能会从用户输入中接收这些值。

文件名: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 18-1:混合 if letelse ifelse if letelse

如果用户指定了喜欢的颜色,则该颜色用作背景色。如果未指定喜欢的颜色且今天是星期二,则背景色为绿色。否则,如果用户将其年龄指定为字符串,并且我们可以成功将其解析为数字,则颜色为紫色或橙色,具体取决于数字的值。如果这些条件都不适用,则背景色为蓝色。

此条件结构使我们能够支持复杂的要求。使用我们在此处拥有的硬编码值,此示例将打印 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 循环在模式持续匹配时运行。在 Listing 18-2 中,我们编写了一个 while let 循环,该循环使用 vector 作为堆栈,并以与推送顺序相反的顺序打印 vector 中的值。

fn main() {
    let mut stack = Vec::new();

    stack.push(1);
    stack.push(2);
    stack.push(3);

    while let Some(top) = stack.pop() {
        println!("{top}");
    }
}

Listing 18-2:使用 while let 循环打印值,只要 stack.pop() 返回 Some

此示例打印 3、2,然后打印 1。pop 方法从 vector 中取出最后一个元素并返回 Some(value)。如果 vector 为空,pop 返回 None。只要 pop 返回 Somewhile 循环就会继续运行其块中的代码。当 pop 返回 None 时,循环停止。我们可以使用 while let 从堆栈中弹出每个元素。

for 循环

for 循环中,紧跟关键字 for 后面的值是一个模式。例如,在 for x in y 中,x 是模式。Listing 18-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 18-3:在 for 循环中使用模式来解构元组

Listing 18-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 语句中。例如,考虑使用 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 的模式匹配方面,请考虑 Listing 18-4,它使用带有 let 的模式来解构元组。

fn main() {
    let (x, y, z) = (1, 2, 3);
}

Listing 18-4:使用模式来解构元组并一次创建三个变量

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

如果模式中元素的数量与元组中元素的数量不匹配,则整体类型将不匹配,我们将收到编译器错误。例如,Listing 18-5 显示了尝试将具有三个元素的元组解构为两个变量,这将不起作用。

fn main() {
    let (x, y) = (1, 2, 3);
}

Listing 18-5:错误地构造了一个模式,其变量与元组中元素的数量不匹配

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

$ 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

要修复此错误,我们可以使用 _.. 忽略元组中的一个或多个值,正如你将在 “在模式中忽略值”部分中看到的那样。如果问题是我们模式中有太多变量,则解决方案是通过删除变量来使类型匹配,以便变量的数量等于元组中元素的数量。

函数参数

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

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

fn main() {}

Listing 18-6:函数签名在参数中使用模式

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

文件名: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 18-7:具有解构元组的参数的函数

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

我们也可以在闭包参数列表中使用模式,方式与函数参数列表相同,因为闭包与函数类似,如第 13 章中所述。

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