UP | HOME

模式匹配

Table of Contents

模式是 Rust 中特殊的语法,它用来匹配类型中的结构,无论类型是简单还是复杂。结合使用模式和 match 表达式以及其他结构可以提供更多对程序控制流的支配权。模式由如下一些内容组合而成:

这些部分描述了要处理的数据的形状,接着可以用其匹配值来决定程序是否拥有正确的数据来运行特定部分的代码。通过将一些值与模式相比较来使用它。如果模式匹配这些值,对值部分进行相应处理

  回忆一下曾经讨论 match 表达式时像硬币分类器那样使用模式

  如果数据符合这个形状,就可以使用这些命名的片段。如果不符合,与该模式相关的代码则不会运行

可能会用到模式的位置

模式出现在 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 中的模式不匹配时运行。

下面展示了也可以组合并匹配 if let、else if 和 else if let 表达式。这相比 match 表达式一次只能将一个值与模式比较提供了更多灵活性;一系列 if let、else if、else if let 分支并不要求其条件相互关联

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");
    }
}
     如果用户指定了中意的颜色,将使用其作为背景颜色

     如果今天是星期二,背景颜色将是绿色

     如果用户指定了他们的年龄字符串并能够成功将其解析为数字的话,将根据这个数字使用紫色或者橙色

     最后,如果没有一个条件符合,背景颜色将是蓝色

     这个条件结构允许支持复杂的需求。使用这里硬编码的值,例子会打印出 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 循环。下面展示了一个使用 while let 的例子,它使用 vector 作为栈并以先进后出的方式打印出 vector 中的值:

let mut stack = Vec::new();

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

while let Some(top) = stack.pop() {
    println!("{}", top);
}
这个例子会打印出 3、2 接着是 1

pop 方法取出 vector 的最后一个元素并返回 Some(value)。如果 vector 是空的,它返回 None

while 循环只要 pop 返回 Some 就会一直运行其块中的代码,一旦其返回 None,while 循环停止

这样可以使用 while let 来弹出栈中的每一个元素

for 循环

for 可以获取一个模式。在 for 循环中,模式是 for 关键字直接跟随的值,正如 for x in y 中的 x。下面展示了如何使用 for 循环来解构,或拆开一个元组作为 for 循环的一部分:

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

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

示例会打印出:

a is at index 0
b is at index 1
c is at index 2
     这里使用 enumerate 方法适配一个迭代器来产生一个值和其在迭代器中的索引,他们位于一个元组中

     第一个 enumerate 调用会产生元组 (0, 'a')。当这个值匹配模式 (index, value),index 将会是 0 而 value 将会是 'a',并打印出第一行输出

let 语句

以前只明确的讨论过通过 match 和 if let 使用模式,不过事实上也在别地地方使用过模式,包括 let 语句。例如,考虑一下这个直白的 let 变量赋值:

let x = 5;

这正是在使用模式。let 语句更为正式的样子如下:

let PATTERN = EXPRESSION;

像 let x = 5; 这样的语句中变量名位于 PATTERN 位置,变量名不过是形式特别朴素的模式。编译器将表达式与模式比较,并为任何找到的名称赋值

所以例如 let x = 5; 的情况,x 是一个模式代表 “将匹配到的值绑定到变量 x”,同时因为名称 x 是整个模式,这个模式实际上等于 “将任何值绑定到变量 x,不管值是什么”

为了更清楚的理解 let 的模式匹配方面的内容,考虑下面使用 let 和模式解构一个元组:

let (x, y, z) = (1, 2, 3);
     这里将一个元组与模式匹配。Rust 会比较值 (1, 2, 3) 与模式 (x, y, z) 并发现此值匹配这个模式

     在这个例子中,将会把 1 绑定到 x,2 绑定到 y 并将 3 绑定到 z

     可以将这个元组模式看作是将三个独立的变量模式结合在一起

如果模式中元素的数量不匹配元组中元素的数量,则整个类型不匹配,并会得到一个编译时错误:

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

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

error[E0308]: mismatched types
 --> src/main.rs:2:9
  |
2 |     let (x, y) = (1, 2, 3);
  |         ^^^^^^ expected a tuple with 3 elements, found one with 2 elements
  |
  = note: expected type `({integer}, {integer}, {integer})`
             found type `(_, _)`
     如果希望忽略元组中一个或多个值,也可以使用 _ 或 ..,如 “忽略模式中的值” 部分所示

     如果问题是模式中有太多的变量,则解决方法是通过去掉变量使得变量数与元组中元素数相等

函数参数

函数参数也可以是模式。下面的代码声明了一个叫做 foo 的函数,它获取一个 i32 类型的参数 x,现在这看起来应该很熟悉:

fn foo(x: i32) {
    // 代码
}

x 部分就是一个模式。类似于之前对 let 所做的,可以在函数参数中匹配元组。传递给函数的元组拆分为值:

fn print_coordinates(&(x, y): &(i32, i32)) {
    println!("Current location: ({}, {})", x, y);
}

fn main() {
    let point = (3, 5);
    print_coordinates(&point);
}
   这会打印出 Current location: (3, 5)。值 &(3, 5) 会匹配模式 &(x, y),如此 x 得到了值 3,而 y得到了值 5

同样也可以在闭包参数列表中使用模式

模式是否会匹配失效

模式有两种形式:

  • refutable:对某些可能的值进行匹配会失败的模式, if letwhile let 表达式被限制为只能接受可反驳的模式,因为根据定义他们意在处理可能的失败:条件表达式的功能就是根据成功或失败执行不同的操作
    一个这样的例子便是 if let Some(x) = a_value 表达式中的 Some(x);如果变量 a_value 中的值是 None 而不是 Some,那么 Some(x) 模式不能匹配
  • irrefutable:能匹配任何传递的可能值的模式, 函数参数let 语句和 for 循环 只能接受不可反驳的模式,因为通过不匹配的值程序无法进行有意义的工作
    一个例子就是 let x = 5; 语句中的 x,因为 x 可以匹配任何值所以不可能会失败

看看一个尝试在 Rust 要求不可反驳模式的地方使用可反驳模式以及相反情况的例子。下面有一个 let 语句,不过模式被指定为可反驳模式 Some(x),当然这不能编译:

let Some(x) = some_option_value;

如果 some_option_value 的值是 None,其不会成功匹配模式 Some(x),表明这个模式是可反驳的。然而 let 语句只能接受不可反驳模式因为代码不能通过 None 值进行有效的操作。Rust 会在编译时抱怨在要求不可反驳模式的地方使用可反驳模式:

error[E0005]: refutable pattern in local binding: `None` not covered
 -->
  |
3 | let Some(x) = some_option_value;
  |     ^^^^^^^ pattern `None` not covered
  因为没有覆盖(也不可能覆盖!)到模式 Some(x) 的每一个可能的值, 所以 Rust 会合理地抗议

为了修复在需要不可反驳模式的地方使用可反驳模式的情况,可以修改使用模式的代码:不同于使用 let,可以使用 if let。如此,如果模式不匹配,大括号中的代码将被忽略,其余代码保持有效:

if let Some(x) = some_option_value {
    println!("{}", x);
}

如果为 if let 提供了一个总是会匹配的模式,编译器会给出一个警告:

if let x = 5 {
    println!("{}", x);
};

Rust 会抱怨将不可反驳模式用于 if let 是没有意义的:

warning: irrefutable if-let pattern
 --> <anon>:2:5
  |
2 | /     if let x = 5 {
3 | |     println!("{}", x);
4 | | };
  | |_^
  |
  = note: #[warn(irrefutable_let_patterns)] on by default

基于此,match匹配分支必须使用可反驳模式,除了最后一个分支需要使用能匹配任何剩余值的不可反驳模式

Rust允许在只有一个匹配分支的match中使用不可反驳模式,不过这么做不是特别有用,并可以被更简单的 let 语句替代

通常无需担心可反驳和不可反驳模式的区别,不过确实需要熟悉可反驳性的概念,这样当在错误信息中看到时就知道如何应对

遇到这些情况,根据代码行为的意图,需要修改模式或者使用模式的结构

模式语法