模式的所有使用场景

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

清单 18-1 显示了还可以混合使用 if letelse ifelse if let 表达式。这样做比 match 表达式提供了更大的灵活性,在 match 表达式中,我们只能表达一个要与模式进行比较的值。此外,Rust 不要求 if letelse ifelse 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, {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 letelse ifelse if letelse

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

这种条件结构允许我们支持复杂的条件需求。使用这里硬编码的值,这个例子将打印使用紫色作为背景颜色

你可以看到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循环只要模式继续匹配就运行。在清单18-2中,我们编写了一个while let循环,它使用向量作为栈,并以与它们被压入相反的顺序打印向量中的值。

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方法从向量中取出最后一个元素并返回Some(value)。如果向量为空,pop返回None。只要pop返回Somewhile循环就会继续运行其代码块中的代码。当pop返回None时,循环停止。我们可以使用while let将栈中的每个元素都弹出。

for 循环

for循环中,紧跟在关键字for后面的值是一个模式。例如,在for x in y中,x是模式。清单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}");
    }
}

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

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

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
    Finished dev [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的模式匹配方面,请考虑清单18-4,它使用let中的模式来解构元组。

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

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

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

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

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

清单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

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

函数参数

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

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

fn main() {}

清单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:一个参数解构元组的函数

这段代码打印当前位置:(3, 5)。值&(3, 5)与模式&(x, y)匹配,所以x的值是3y的值是5

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

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