模式可使用的所有地方

Beginner
立即练习

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

简介

欢迎来到「模式可使用的所有地方」实验。本实验是 《Rust 程序设计语言》 的一部分。你可以在 LabEx 中练习 Rust 技能。

在本实验中,我们将探索 Rust 中可以使用模式的所有地方。

模式可使用的所有地方

模式在 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 表达式的一个要求是,它们需要是 _详尽无遗的_,也就是说,必须考虑 match 表达式中值的所有可能性。确保涵盖每种可能性的一种方法是为最后一个分支使用一个通配模式:例如,一个匹配任何值的变量名永远不会失败,因此涵盖了所有剩余的情况。

特殊模式 _ 可以匹配任何内容,但它永远不会绑定到变量,因此它通常用于最后一个匹配分支。例如,当你想要忽略任何未指定的值时,_ 模式可能会很有用。我们将在「忽略模式中的值」中更详细地介绍 _ 模式。

条件 if let 表达式

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

清单 18-1 展示了也可以混合使用 if let、else if 和 else if let 表达式。这样做比 match 表达式给我们提供了更多的灵活性,在 match 表达式中我们只能表达一个要与模式进行比较的值。此外,Rust 并不要求一系列 if let、else if 和 else if let 分支中的条件相互关联。

清单 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}, 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");
    }
}

清单 18-1:混合使用 if let、else if、else if let 和 else

如果用户指定了最喜欢的颜色 [1],那么该颜色将被用作背景 [2]。如果没有指定最喜欢的颜色且今天是星期二 [3],则背景颜色为绿色 [4]。否则,如果用户将他们的年龄指定为字符串并且我们可以成功地将其解析为数字 [5],那么根据数字的值,颜色要么是紫色 [7] 要么是橙色 [8] [6]。如果这些条件都不适用 [9],则背景颜色为蓝色 [10]。

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

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

使用 if let 表达式的缺点是编译器不会检查是否详尽无遗,而对于 match 表达式它会检查。如果我们省略了最后一个 else 块 [9],从而遗漏了处理某些情况,编译器不会提醒我们可能存在的逻辑错误。

while let 条件循环

while let 条件循环在结构上与 if let 类似,它允许 while 循环只要模式持续匹配就一直运行。在清单 18-2 中,我们编写了一个 while let 循环,它将一个向量用作栈,并以与入栈相反的顺序打印向量中的值。

文件名:src/main.rs

let mut stack = Vec::new();

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

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

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

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

for 循环

在 for 循环中,紧跟在关键字 for 后面的值是一个模式。例如,在 for x in y 中,x 就是模式。清单 18-3 展示了如何在 for 循环中使用模式来进行 _解构_,也就是将一个元组拆开,作为 for 循环的一部分。

文件名:src/main.rs

let v = vec!['a', 'b', 'c'];

for (index, value) in v.iter().enumerate() {
    println!("{value} is at index {index}");
}

清单 18-3:在 for 循环中使用模式解构元组

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

a is at index 0
b is at index 1
c is at index 2

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

let 语句

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

let x = 5;

每次你使用这样的 let 语句时,你都在使用模式,尽管你可能没有意识到这一点!更正式地说,一个 let 语句看起来像这样:

let PATTERN = EXPRESSION;

在像 let x = 5; 这样的语句中,模式部分是一个变量名,这个变量名只是模式的一种特别简单的形式。Rust 将表达式与模式进行比较,并为它找到的任何名字赋值。所以,在 let x = 5; 的例子中,x 是一个模式,意思是“将这里匹配到的内容绑定到变量 x 上”。因为名字 x 就是整个模式,所以这个模式实际上意味着“将任何值都绑定到变量 x 上”。

为了更清楚地看到 let 的模式匹配方面,考虑清单 18-4,它使用带有 let 的模式来解构一个元组。

let (x, y, z) = (1, 2, 3);

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

在这里,我们将一个元组与一个模式进行匹配。Rust 将值 (1, 2, 3) 与模式 (x, y, z) 进行比较,发现值与模式匹配,因为它看到两者的元素数量相同,所以 Rust 将 1 绑定到 x,2 绑定到 y,3 绑定到 z。你可以将这个元组模式看作是在其中嵌套了三个单独的变量模式。

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

let (x, y) = (1, 2, 3);

清单 18-5:构造一个变量与元组元素数量不匹配的模式

尝试编译这段代码会导致以下类型错误:

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 `(_, _)`

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

函数参数

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

fn foo(x: i32) {
    // 代码写在这里
}

清单 18-6:在参数中使用模式的函数签名

x 部分就是一个模式!就像我们在 let 中做的那样,我们可以将函数参数中的元组与模式进行匹配。清单 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);
}

清单 18-7:一个参数解构元组的函数

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

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

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

总结

恭喜你!你已经完成了“模式可使用的所有地方”实验。你可以在 LabEx 中练习更多实验来提升你的技能。