所有可能会用到模式的位置

ch18-01-all-the-places-for-patterns.md
commit 4ca9e513e532a4d229ab5af7dfcc567129623bf4

模式出现在 Rust 的很多地方。你已经在不经意间使用了很多模式!本部分是一个所有有效模式位置的参考。

match 分支

如第六章所讨论的,一个模式常用的位置是 match 表达式的分支。在形式上 match 表达式由 match 关键字、用于匹配的值和一个或多个分支构成。这些分支包含一个模式和在值匹配分支的模式时运行的表达式:

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

穷尽性和默认模式 _

match 表达式必须是穷尽的。当我们把所有分支的模式都放在一起,match 表达式所有可能的值都应该被考虑到。一个确保覆盖每个可能值的方法是在最后一个分支使用捕获所有的模式,比如一个变量名。一个匹配任何值的名称永远也不会失败,因此可以覆盖之前分支模式匹配剩下的情况。

这有一个额外的模式经常被用于结尾的分支:_。它匹配所有情况,不过它从不绑定任何变量。这在例如只希望在某些模式下运行代码而忽略其他值的时候很有用。

if let 表达式

第六章讨论过了 if let 表达式,以及它是如何成为编写等同于只关心一个情况的 match 语句的简写的。if let 可以对应一个可选的 else 和代码在 if let 中的模式不匹配时运行。

列表 18-1 展示了甚至可以组合并匹配 if letelse ifelse if let。这些代码展示了一系列针对不同条件的检查来决定背景颜色应该是什么。为了达到这个例子的目的,我们创建了硬编码值的变量,在真实程序中则可能由询问用户获得。如果用户指定了中意的颜色,我们将使用它作为背景颜色。如果今天是星期二,背景颜色将是绿色。如果用户指定了他们的年龄字符串并能够成功将其解析为数字的话,我们将根据这个数字使用紫色或者橙色。最后,如果没有一个条件符合,背景颜色将是蓝色:

文件名: 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", color);
    } 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 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 直到大括号开始的新作用域才是有效的。

另外注意这样有很多情况的条件并没有 match 表达式强大,因为其穷尽性没有为编译器所检查。如果去掉最后的 else 块而遗漏处理一些情况,编译器也不会报错。这个例子可能过于复杂以致难以重写为一个可读的 match,所以需要额外注意处理了所有的情况,因为编译器不会为我们检查穷尽性。

while let

一个与 if let 类似的结构体是 while let:它允许只要模式匹配就一直进行 while 循环。列表 18-2 展示了一个使用 while let 的例子,它使用 vector 作为栈并打以先进后出的方式打印出 vector 中的值:


# #![allow(unused_variables)]
#fn main() {
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 方法取出 vector 的最后一个元素并返回Some(value),如果 vector 是空的,它返回 Nonewhile 循环只要 pop 返回 Some 就会一直运行其块中的代码。一旦其返回 Nonewhile循环停止。我们可以使用 while let 来弹出栈中的每一个元素。

for 循环

for 循环,如同第三章所讲的,是 Rust 中最常见的循环结构。那一章所没有讲到的是 for 可以获取一个模式。列表 18-3 中展示了如何使用 for 循环来解构一个元组。enumerate 方法适配一个迭代器来产生元组,其包含值和值的索引:


# #![allow(unused_variables)]
#fn main() {
let v = vec![1, 2, 3];

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

列表 18-3: 在 for 循环中使用模式来解构 enumerate 返回的元组

这会打印出:

1 is at index 0
2 is at index 1
3 is at index 2

第一个 enumerate 调用会产生元组 (0, 1)。当这个匹配模式 (index, value)index 将会是 0 而 value 将会是 1。

let 语句

matchif let 都是本书之前明确讨论过的使用模式的位置,不过他们不是仅有的使用过模式的地方。例如,考虑一下这个直白的 let 变量赋值:


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

本书进行了不下百次这样的操作。你可能没有发觉,不过你这正是在使用模式!let 语句更为正式的样子如下:

let PATTERN = EXPRESSION;

我们见过的像 let x = 5; 这样的语句中变量名位于 PATTERN 位置;变量名不过是形式特别朴素的模式。

通过 let,我们将表达式与模式比较,并为任何找到的名称赋值。所以例如 let x = 5; 的情况,x 是一个模式代表“将匹配到的值绑定到变量 x”。同时因为名称 x 是整个模式,这个模式实际上等于“将任何值绑定到变量 x,不过它是什么”。

为了更清楚的理解 let 的模式匹配的方面,考虑列表 18-4 中使用 let 和模式解构一个元组:


# #![allow(unused_variables)]
#fn main() {
let (x, y, z) = (1, 2, 3);
#}

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

这里有一个元组与模式匹配。Rust 会比较值 (1, 2, 3) 与模式 (x, y, z) 并发现值匹配这个模式。在这个例子中,将会把 1 绑定到 x2 绑定到 y3 绑定到 z。你可以将这个元组模式看作是将三个独立的变量模式结合在一起。

在第十六章中我们见过另一个解构元组的例子,列表 16-6 中,那里解构 mpsc::channel() 的返回值为 tx(发送者)和 rx(接收者)。

函数参数

类似于 let,函数参数也可以是模式。列表 18-5 中的代码声明了一个叫做 foo 的函数,它获取一个 i32 类型的参数 x,这看起来应该很熟悉:


# #![allow(unused_variables)]
#fn main() {
fn foo(x: i32) {
    // code goes here
}
#}

列表 18-5: 在参数中使用模式的函数签名

x 部分就是一个模式!类似于之前对 let 所做的,可以在函数参数中匹配元组。列表 18-6 展示了如何可以将传递给函数的元组拆分为值:

文件名: 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-6: 一个在参数中解构元组的函数

这会打印出 Current location: (3, 5)。当传递值 &(3, 5)print_coordinates 时,这个值会匹配模式 &(x, y)x 得到了值 3,而 y得到了值 5。

因为如第十三章所讲闭包类似于函数,也可以在闭包参数中使用模式。

在这些可以使用模式的位置中的一个区别是,对于 for 循环、let 和函数参数,其模式必须是 irrefutable 的。接下来让我们讨论这个。