Rust 基础(七)

十八、模式和匹配(Patterns and Matching

模式是Rust中的一种特殊语法,用于匹配复杂类型和简单类型的结构。将模式与match 表达式和其他构造结合使用,可以更好地控制程序的控制流。一个模式由以下的一些组合组成:

  • 文字 (Literals)
  • 析构数组、枚举、结构体或元组 (Destructured arrays, enums, structs, or tuples)
  • 变量 (Variables)
  • 通配符 (Wildcards)
  • 占位符 (Placeholders)

一些示例模式包括x(a, 3)Some(Color::Red)在模式有效的上下文中,这些组件描述数据的形状。然后,我们的程序将值与模式相匹配,以确定它是否具有继续运行特定代码段所需的正确数据形状。

要使用模式,我们将其与某个值进行比较。如果模式与值匹配,则在代码中使用值部分。回想第6章中使用模式的match表达式,例如硬币分拣机的例子。如果值符合模式的形状,我们可以使用已命名的片段。如果没有,则与模式相关联的代码将无法运行。

本章是与模式相关的所有内容的参考。我们将讨论使用模式的有效位置、可反驳模式和不可反驳模式之间的区别,以及您可能看到的不同类型的模式语法。在本章末尾,您将知道如何使用模式以清晰的方式表达许多概念。

18.1 模式可以使用的所有位置

在Rust中,模式会在许多地方呈现,而且您已经使用了它们很多次而没有意识到这一点!本节讨论模式有效的所有地方。

18.1.1 匹配 arm

正如在第6章中所讨论的,我们在match 表达式的 arm 中使用模式。从形式上讲,match 表达式被定义为关键字match、要匹配的value和一个或多个匹配 arm ,这些匹配 arm由一个模式和一个表达式组成,如果值匹配该臂的模式则运行该表达式,如下所示:

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 表达式中值的所有可能性。确保您已经涵盖了所有可能性的一种方法是为最后一个 arm 拥有一个全面的模式(catchall pattern):例如,匹配任何值的变量名永远不会失败,因此涵盖了所有剩余的情况。

特定的模式_将匹配任何东西,但它从不绑定到变量,因此它经常用于最后的匹配 arm 。例如,当您想要忽略任何未指定的值时,_ pattern可以很有用。我们将在本章后面的“忽略模式中的值”一节中更详细地讨论_ pattern。

18.1.2 Conditional if let Expressions

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

示例18-1显示了还可以混合和匹配if letelse ifelse if let表达式。这样做给我们提供了比match表达式更大的灵活性,在match表达式中,我们只能表示一个值来与模式进行比较。此外,Rust并不要求if let, else if, else if let中的条件相互关联。

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

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

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

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

您可以看到if let也可以像match arms一样引入遮蔽变量:if let Ok(age) = age引入一个新的遮蔽变量age,其中包含Ok变量中的值。这意味着我们需要将if age > 30条件放在该块中:我们不能将这两个条件合并为if let Ok(age) = age && age > 30。我们想要与30进行比较的遮蔽变量age直到新作用域以花括号开始才有效。

使用if let表达式的缺点是编译器不会检查耗尽性,而使用match表达式则会检查。如果我们忽略了最后一个else块,从而错过了处理某些情况,编译器就不会提醒我们可能的逻辑错误

18.1.3 while let 条件循环

if let的结构类似,while let条件循环允许while循环运行,只要模式继续匹配。在示例18-2中,我们编写了一个使用vector作为堆栈的while let循环,并按相反的推入顺序打印vector中的值。
18-2:

    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为空,pop返回None。只要pop返回Some, while循环就会继续在它的块中运行代码。当pop返回None时,循环停止。我们可以使用while let来弹出栈中的所有元素。

18.1.4 for 循环

for循环中,直接跟在for关键字后面的值是一个模式。例如,在for x in y中,x是模式。示例18-3演示了如何在for循环中使用一个模式来解构或分解元组作为for循环的一部分。

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

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

The code in Listing 18-3 will print the following:

$ 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将为0,value将为'a',打印输出的第一行。

18.1.5 let 语句

在本章之前,我们只显式地讨论了在matchif let中使用模式,但实际上,我们在其他地方也使用过模式,包括在let语句中。例如,考虑下面用let直接赋值的变量:

let x = 5;

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

let PATTERN = EXPRESSION;

在诸如let x = 5;如果在PATTERN插槽中有一个变量名,那么变量名就是模式的一种特别简单的形式。Rust将表达式与模式进行比较,并分配它找到的任何名称。例如,let x = 5; 中, x是一个模式,它的意思是“将匹配的内容绑定到变量x上”。因为名称x是整个模式,所以这个模式实际上意味着“将所有内容绑定到变量x上,不管它的值是什么。”

要更清楚地看到let的模式匹配方面,请考虑示例18-4,其中使用一个带有let的模式来解构元组。
18-4:

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

这里,我们将一个元组与一个模式相匹配。Rust将值(1,2,3)与模式(x, y, z)进行比较,并查看值是否与模式匹配,因此Rust将1绑定到x, 2绑定到y, 3绑定到z。您可以将此元组模式视为在其中嵌套了三个单独的变量模式。

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

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

Attempting to compile this code results in this type error:

$ 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` due to previous error

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

18.1.6 函数参数

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

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

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

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

我们还可以像在函数形参表中那样在闭包形参表中使用模式,因为闭包类似于函数,如第13章所述。

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

18.2 Refutability(可反驳性):模式是否可能不匹配

模式有两种形式:可反驳的(refutable)和不可反驳的(irrefutable)。匹配任何可能传递的值的模式是不可反驳的(irrefutable)。一个例子是let x = 5;中的x因为x匹配任何东西,因此不可能匹配失败。无法匹配某些可能值的模式是可以反驳的。一个例子是表达式if let Some(x) = a_value中的Some(x),因为如果a_value变量中的值是None而不是Some,则Some(x)模式将不匹配。

函数形参、let语句和for循环只能接受无可反驳的模式,因为当值不匹配时,程序无法执行任何有意义的操作if letwhile let表达式接受可反驳和不可反驳的模式,但编译器对不可辩驳的模式发出警告,因为根据定义,它们旨在处理可能的失败:条件的功能在于根据成功或失败执行不同的能力。

一般来说,您不必担心可反驳模式和不可反驳模式之间的区别;但是,您确实需要熟悉可驳性的概念,以便在错误消息中看到它时能够作出响应。在这些情况下,您需要根据代码的预期行为更改模式或使用模式的构造。

让我们看一个例子,当我们尝试使用一个可反驳的模式(Rust需要一个不可反驳的模式,反之亦然)时会发生什么。示例18-8显示了一个let语句,但是对于我们指定了Some(x)的模式,这是一个可反驳的模式。如您所料,此代码将无法编译。

 let Some(x) = some_option_value;

如果some_option_valueNone值,它将无法匹配Some(x)模式,这意味着该模式是可反驳的。然而,let语句只能接受一个不可反驳的模式,因为对于None值,代码不能做任何有效的事情。在编译时,Rust会抱怨我们在需要一个不可反驳的模式的地方尝试使用了一个可反驳的模式:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0005]: refutable pattern in local binding: `None` not covered
   --> src/main.rs:3:9
    |
3   |     let Some(x) = some_option_value;
    |         ^^^^^^^ pattern `None` not covered
    |
    = note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
    = note: for more information, visit https://doc.rust-lang.org/book/ch18-02-refutability.html
note: `Option<i32>` defined here
    = note: the matched value is of type `Option<i32>`
help: you might want to use `if let` to ignore the variant that isn't matched
    |
3   |     let x = if let Some(x) = some_option_value { x } else { todo!() };
    |     ++++++++++                                 ++++++++++++++++++++++

For more information about this error, try `rustc --explain E0005`.
error: could not compile `patterns` due to previous error

因为我们没有用Some(x)模式覆盖(也不能覆盖!)每个有效值,Rust自然会产生一个编译器错误。

如果我们有一个可反驳的模式,而需要一个不可反驳的模式,我们可以通过更改使用该模式的代码来修复它:我们可以使用If let而不是let。然后,如果模式不匹配,代码将跳过花括号中的代码,从而提供一种继续有效的方法。示例18-9显示了如何修复示例18-8中的代码。

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

我们已经让代码退出了!这段代码是完全有效的,尽管这意味着我们不能在不收到错误的情况下使用无可辩驳的模式。如果我们输入If let一个总是匹配的模式,例如x,如示例18-10所示,编译器将给出一个警告。

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

Rust抱怨说,if let它有一个无可辩驳的模式,使用它是没有意义的:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
warning: irrefutable `if let` pattern
 --> src/main.rs:2:8
  |
2 |     if let x = 5 {
  |        ^^^^^^^^^
  |
  = note: `#[warn(irrefutable_let_patterns)]` on by default
  = note: this pattern will always match, so the `if let` is useless
  = help: consider replacing the `if let` with a `let`

warning: `patterns` (bin "patterns") generated 1 warning
    Finished dev [unoptimized + debuginfo] target(s) in 0.39s
     Running `target/debug/patterns`
5

因此,匹配 arm 必须使用可辩驳的模式,但最后一个arm 除外,它应该使用不可反驳的模式匹配任何剩余值。Rust允许我们在只有一只arm的匹配中使用无可辩驳的模式,但这种语法并不是特别有用,可以用更简单的let语句代替。

现在您已经知道了在哪里使用模式以及可反驳模式和不可反驳模式之间的区别,下面让我们介绍可以用来创建模式的所有语法。

18.3 Pattern 语法

在本节中,我们将收集模式中所有有效的语法,并讨论为什么以及何时需要使用每种语法。

18.3.1 匹配字面值

正如您在第6章中看到的,您可以直接根据字面量匹配模式。下面的代码给出了一些示例:

fn main() {
    let x = 1;

    match x {
        1 => println!("one"),
        2 => println!("two"),
        3 => println!("three"),
        _ => println!("anything"),
    }
}

这段代码打印one,因为x中的值是1。当您希望代码在获得特定具体值时采取操作时,此语法非常有用。

18.3.2 匹配命名变量

命名变量(Named variables)是匹配任何值的不可反驳的模式(irrefutable patterns),我们在书中多次使用它们。但是,在match 表达式中使用命名变量时会有一个复杂的问题。因为match开始一个新的作用域,在match表达式中声明为模式的一部分的变量将掩盖match构造外同名的变量,所有变量都是如此。在示例18-11中,我们声明了一个值为Some(5)的变量x和一个值为10的变量y。然后,我们在值x上创建一个match表达式。查看匹配 arm 中的模式并println!最后,在运行这段代码或进一步阅读之前,尝试弄清楚代码将打印什么。
18-11:

    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Got 50"),
        Some(y) => println!("Matched, y = {y}"),
        _ => println!("Default case, x = {:?}", x),
    }

    println!("at the end: x = {:?}, y = {y}", x);

让我们看看match表达式运行时会发生什么。第一个匹配 arm 中的模式与x的定义值不匹配,因此代码继续。

第二个匹配臂中的模式引入了一个名为y的新变量,该变量将匹配Some值中的任何值。因为我们在match表达式中的一个新的作用域中,这是一个新的y变量,而不是我们在开始声明的值为10y。这个新的y绑定将匹配Some中的任何值,也就是我们在x中拥有的值。因此,这个新的y绑定到xSome的内部值。该值为5,因此该 arm 的表达式执行并打印Matched, y = 5

如果x是一个None值而不是Some(5),那么前两臂中的模式将不匹配,因此该值将与下划线匹配。我们没有在下划线臂的模式中引入x变量,因此表达式中的x仍然是没有被遮蔽的外部x。在这种假设的情况下,匹配将打印Default case, x = None

当匹配表达式完成时,它的作用域结束,内部y的作用域也结束。最后打印:at the end: x = Some(5), y = 10

为了创建一个比较外部xy值的match表达式,而不是引入一个遮蔽变量,我们需要使用一个匹配守卫条件。我们将在后面的“用匹配守卫附加条件”部分讨论匹配守卫。

18.3.3 Multiple Patterns

match表达式中,可以使用|语法(即模式或操作符)匹配多个模式。例如,在下面的代码中,我们将x的值与匹配臂进行匹配,其中第一个 arm 有一个or选项,这意味着如果x的值与该 arm 中的任何一个值匹配,则该 arm 的代码将运行:

    let x = 1;

    match x {
        1 | 2 => println!("one or two"),
        3 => println!("three"),
        _ => println!("anything"),
    }

This code prints one or two.

18.3.4 使用..=匹配值的范围

. .=语法允许我们匹配一个包含范围的值。在下面的代码中,当一个模式匹配给定范围内的任何值时,该 arm 将执行:

    let x = 5;

    match x {
        1..=5 => println!("one through five"),
        _ => println!("something else"),
    }

如果x12345,第一个arm 将匹配。这种语法对于多个匹配值来说比使用|运算符来表达相同的思想更方便;如果我们要使用|,我们必须指定1 | 2 | 3 | 4 | 5。指定一个范围要短得多,特别是如果我们想匹配11000之间的任何数字!

编译器在编译时检查范围是否为空,因为Rust可以判断范围是否为空的唯一类型是charnumeric值,所以范围只允许具有numericchar值。

下面是一个使用char值范围的例子:

    let x = 'c';

    match x {
        'a'..='j' => println!("early ASCII letter"),
        'k'..='z' => println!("late ASCII letter"),
        _ => println!("something else"),
    }

Rust可以判断出’c’在第一个模式的范围内,并打印early ASCII letter

18.3.5 解构来分解值

我们还可以使用模式来解构结构、枚举和元组,以使用这些值的不同部分。让我们遍历每个值。

解构结构

示例18-12显示了一个Point结构体,它有两个字段xy,我们可以使用let语句的模式将它们分开。

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };

    let Point { x: a, y: b } = p;
    assert_eq!(0, a);
    assert_eq!(7, b);
}

这段代码创建了变量ab,它们与p结构体的xy字段的值相匹配。这个例子表明模式中的变量名不必与结构的字段名匹配。但是,通常将变量名与字段名相匹配,以便更容易记住哪个变量来自哪个字段。由于这种常用的用法,又因为写let Point {x: x, y: y} = p;Rust为匹配结构体字段的模式提供了一种简写:你只需要列出结构字段的名称,从模式中创建的变量将具有相同的名称。示例18-13的行为方式与示例18-12中的代码相同,但是let模式中创建的变量是xy,而不是ab

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };

    let Point { x, y } = p;
    assert_eq!(0, x);
    assert_eq!(7, y);
}

这段代码创建了变量xy,它们与p变量的xy字段相匹配。结果是变量xy包含了p结构的值。

我们还可以将字面量作为结构体模式的一部分进行解构,而不是为所有字段创建变量。这样做允许我们测试某些字段的特定值,同时创建变量来分解其他字段。

在示例18-14中,我们有一个match 表达式,它将Point值分为三种情况:直接位于x轴上的点(当y = 0时为真),位于y轴上的点(x = 0),或者两者都不是。

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };

    match p {
        Point { x, y: 0 } => println!("On the x axis at {}", x),
        Point { x: 0, y } => println!("On the y axis at {}", y),
        Point { x, y } => println!("On neither axis: ({}, {})", x, y),
    }
}

第一个arm 将匹配x轴上的任何点,如果指定了y字段匹配且y字段的值匹配文字0。该模式仍然创建了一个x变量,我们可以在这个arm 的代码中使用它。

类似地,第二 arm 通过指定x字段的值为0匹配y轴上的任何点,并为y字段的值创建一个变量y。第三个 arm 没有指定任何字面量,因此它匹配任何其他Point,并为xy字段创建变量。

在这个例子中,由于x包含0,值p与第二个 arm 匹配,所以这段代码将打印On the y axis at 7

记住,match 表达式一旦找到第一个匹配模式就会停止检查手臂,所以即Point { x: 0, y: 0}x轴和y轴上,这段代码只会打印On the x axis at 0

解构枚举

在本书中,我们已经对枚举进行了解构(例如,第6章中的示例6-5),但还没有明确讨论解构枚举的模式对应于定义枚举中存储的数据的方式。例如,在示例18-15中,我们使用示例6-2中的Messageenum,并使用将解构每个内部值的模式编写一个match

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn main() {
    let msg = Message::ChangeColor(0, 160, 255);

    match msg {
        Message::Quit => {
            println!("The Quit variant has no data to destructure.")
        }
        Message::Move { x, y } => {
            println!(
                "Move in the x direction {} and in the y direction {}",
                x, y
            );
        }
        Message::Write(text) => println!("Text message: {}", text),
        Message::ChangeColor(r, g, b) => println!(
            "Change the color to red {}, green {}, and blue {}",
            r, g, b
        ),
    }
}

将打印Change the color to red 0, green 160, and blue 255。尝试更改msg的值以查看其他分支运行的代码。

对于没有任何数据的枚举变量,如Message::Quit,我们不能进一步解构值。我们只能匹配字面Message::Quit值,并且没有变量在该模式中。

对于类结构体的枚举变量,例如Message::Move,我们可以使用与指定的模式类似的模式来匹配结构。在变量名之后,我们放置花括号,然后列出带有变量的字段,这样我们就可以将这些字段分开,以便在该arm的代码中使用。在这里,我们使用示例18-13中的简写形式。

对于类似元组的enum变量,比如Message::Write保存一个包含一个元素的元组,Message::ChangeColor保存一个包含三个元素的元组,模式类似于我们指定的匹配元组的模式。模式中的变量数量必须与我们所匹配的变量中的元素数量相匹配。

解构嵌套结构和枚举

到目前为止,我们的示例都是匹配单层结构体或枚举,但匹配也可以用于嵌套项!例如,我们可以重构示例18-15中的代码,以支持ChangeColor消息中的RGBHSV颜色,如示例18-16所示。

enum Color {
    Rgb(i32, i32, i32),
    Hsv(i32, i32, i32),
}

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(Color),
}

fn main() {
    let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));

    match msg {
        Message::ChangeColor(Color::Rgb(r, g, b)) => println!(
            "Change the color to red {}, green {}, and blue {}",
            r, g, b
        ),
        Message::ChangeColor(Color::Hsv(h, s, v)) => println!(
            "Change the color to hue {}, saturation {}, and value {}",
            h, s, v
        ),
        _ => (),
    }
}

match 表达式中第一个 arm 的模式匹配一个Message::ChangeColor enum变量,该变量包含一个Color::Rgb变量;然后模式绑定到三个内部的i32值。第二 arm 的模式也匹配Message::ChangeColor枚举变体,但内部枚举匹配Color::Hsv。我们可以在一个匹配表达式中指定这些复杂的条件,即使涉及到两个枚举。

解构结构体和元组

我们可以用更复杂的方式混合、匹配和嵌套解构模式。下面的例子展示了一个复杂的解构,我们将结构体和元组嵌套在一个元组中,并将所有的原语值解构出来:

fn main() {
    struct Point {
        x: i32,
        y: i32,
    }

    let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
}

这段代码允许我们将复杂类型分解为它们的组成部分,这样我们就可以单独使用我们感兴趣的值。

使用模式进行解构是一种方便的方法,可以将值块(例如来自结构体中每个字段的值)彼此分开使用。

18.3.6 忽略模式中的值

您已经看到,有时忽略模式中的值(例如在match的最后一个 arm )是有用的,以获得一个实际上不做任何事情,但考虑所有剩余可能值的catchall。有几种方法可以忽略模式中的全部值或部分值:使用_ pattern(您已经看到过),在另一个模式中使用_ pattern,使用以下划线开头的名称,或使用..忽略值的其余部分。让我们探讨如何以及为什么使用这些模式。

使用_的忽略全部值

我们使用下划线_作为通配符模式,它将匹配任何值,但不绑定到该值。这作为match表达式中的最后一个 arm 特别有用,但我们也可以在任何模式中使用它,包括函数参数,如示例18-17所示。

fn foo(_: i32, y: i32) {
    println!("This code only uses the y parameter: {}", y);
}

fn main() {
    foo(3, 4);
}

这段代码将完全忽略作为第一个参数传递的值3,并打印This code only uses the y parameter: 4

在大多数情况下,当您不再需要某个特定的函数参数时,您可以更改签名,使其不包括未使用的参数。忽略函数参数在某些情况下尤其有用,例如,当您实现一个 trait 时需要某个类型签名,但实现中的函数体不需要其中一个参数。这样就避免了编译器对未使用的函数参数发出警告,就像使用名称一样。

使用嵌套_忽略值的部分

我们也可以在另一个模式中使用_来忽略一个值的一部分,例如,当我们只想测试一个值的一部分,但在我们想要运行的相应代码中,其他部分没有用处时。示例18-18显示了负责管理设置值的代码。业务需求是不允许用户覆盖现有的自定义设置,但可以取消设置并在当前未设置时为其赋值。

fn main() {
    let mut setting_value = Some(5);
    let new_setting_value = Some(10);

    match (setting_value, new_setting_value) {
        (Some(_), Some(_)) => {
            println!("Can't overwrite an existing customized value");
        }
        _ => {
            setting_value = new_setting_value;
        }
    }

    println!("setting is {:?}", setting_value);
}

这段代码将打印Can't overwrite an existing customized value and then setting is Some(5),然后setting is Some(5)。在第一个匹配arm中,我们不需要匹配或使用Some变量中的值,但我们确实需要测试当setting_valuenew_setting_valueSome变量时的情况。在这种情况下,我们打印不改变setting_value的原因,它不会被改变。

在所有其他由第二个arm中的_模式表示的情况下(如果setting_valuenew_setting_valueNone),我们允许new_setting_value变为setting_value

我们还可以在一个模式中的多个位置使用下划线来忽略特定的值。示例18-19显示了忽略5项元组中的第二个和第四个值的示例。

    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (first, _, third, _, fifth) => {
            println!("Some numbers: {first}, {third}, {fifth}")
        }
    }

这段代码将打印Some numbers: 2, 8, 32,值4和16将被忽略。

变量名开头使用_忽略一个未使用的变量

如果你创建了一个变量,但没有在任何地方使用它,Rust通常会发出警告,因为一个未使用的变量可能是一个bug。然而,有时能够创建一个您还不会使用的变量是有用的,例如当您正在创建原型或刚开始一个项目时。在这种情况下,你可以告诉Rust不要用下划线开头的变量名来警告你这个未使用的变量。在示例18-20中,我们创建了两个未使用的变量,但是在编译这段代码时,我们应该只得到关于其中一个变量的警告。

fn main() {
    let _x = 5;
    let y = 10;
}

这里我们得到了关于不使用变量y的警告,但是我们没有得到关于不使用_x的警告。

请注意,只使用_和使用以下划线开头的名称之间有细微的区别。语法_x仍然将值绑定到变量,而_根本不绑定。为了说明这种区别的重要性,示例18-21给出了一个错误。

    let s = Some(String::from("Hello!"));

    if let Some(_s) = s {
        println!("found a string");
    }

    println!("{:?}", s);

我们将收到一个错误,因为s值仍然会被移动到_s中,这阻止了我们再次使用s。但是,单独使用下划线并不会绑定到值。示例18-22编译时不会出现任何错误,因为s没有被移动到_中。

    let s = Some(String::from("Hello!"));

    if let Some(_) = s {
        println!("found a string");
    }

    println!("{:?}", s);

这段代码工作得很好,因为我们从未将s绑定到任何东西;它没有移动。

..忽略值的剩余部分

对于包含多个部分的值,我们可以使用..语法,使用特定部分而忽略其余部分,从而避免为每个被忽略的值列出下划线.. pattern会忽略值中没有显式匹配的任何部分。在示例18-23中,我们有一个Point结构体,它保存三维空间中的坐标。在match 表达式中,我们希望只对x坐标进行操作,而忽略yz字段中的值。

    struct Point {
        x: i32,
        y: i32,
        z: i32,
    }

    let origin = Point { x: 0, y: 0, z: 0 };

    match origin {
        Point { x, .. } => println!("x is {}", x),
    }

我们列出x值,然后只包括..模式。这比必须列出y: _z: _要快得多,特别是当我们处理具有许多字段的结构体时,而只有一两个字段是相关的。

语法..将扩展到它需要的任意多的值。示例18-24展示了如何使用..用一个元组。

fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (first, .., last) => {
            println!("Some numbers: {first}, {last}");
        }
    }
}

在这段代码中,第一个和最后一个值与firstlast匹配。..将匹配并忽略中间的所有内容。

然而,使用..必须是明确的。如果不清楚哪些值是用来匹配的,哪些值应该被忽略,Rust会给我们一个错误。示例18-25显示了一个使用…不明确的,所以它不会编译。

fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (.., second, ..) => {
            println!("Some numbers: {}", second)
        },
    }
}

When we compile this example, we get this error:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
error: `..` can only be used once per tuple pattern
 --> src/main.rs:5:22
  |
5 |         (.., second, ..) => {
  |          --          ^^ can only be used once per tuple pattern
  |          |
  |          previously used here

error: could not compile `patterns` due to previous error

Rust不可能确定在匹配second 值之前忽略元组中的多少值,然后再匹配second 值之后忽略多少值。这段代码可能意味着我们想要忽略2,将second绑定到4,然后忽略8、16和32;或者我们想忽略2和4,将second绑定到8,然后忽略16和32;等等。变量名second对Rust没有任何特殊意义,所以我们会得到一个编译器错误,因为使用..在这样的两个地方是模棱两可的。

18.3.7 带有匹配守卫符(Match Guards)的额外条件

匹配守卫(match guard )是一个附加的if条件,在match arm 的模式之后指定,它也必须匹配要选择的 arm(相当于) 。匹配守卫符对于表达比单个模式所允许的更复杂的思想非常有用。

条件可以使用模式中创建的变量。示例18-26显示了一个match ,其中第一个 arm 具有Some(x)模式,并且还有一个if x % 2 == 0的匹配守卫(如果数字为偶数则为真)。

    let num = Some(4);

    match num {
        Some(x) if x % 2 == 0 => println!("The number {} is even", x),
        Some(x) => println!("The number {} is odd", x),
        None => (),
    }

这个例子将打印The number 4 is even。当num与第一个 arm 中的模式进行比较时,它是匹配的,因为Some(4)匹配Some(x)。然后match守卫检查x除以2的余数是否等于0,因为它等于0,所以第一个 arm 被选中。

如果num是Some(5),则第一 arm 中的匹配守卫将为假,因为5除以2的余数为1,不等于0。Rust会转到第二支 arm ,它会匹配,因为第二支 arm 没有匹配守卫,因此匹配任何``Some`变体。

没有办法在模式中表达if x % 2 == 0条件,因此match守卫为我们提供了表达这种逻辑的能力。这种额外表达方式的缺点是,当涉及到匹配守卫表达式时,编译器不会尝试检查耗尽性

在示例18-11中,我们提到可以使用匹配守卫来解决模式阴影问题。回想一下,我们在match表达式的模式内创建了一个新变量,而不是在match表达式外使用变量。这个新变量意味着我们不能测试外部变量的值。示例18-27展示了如何使用match守卫来修复这个问题。

fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Got 50"),
        Some(n) if n == y => println!("Matched, n = {n}"),
        _ => println!("Default case, x = {:?}", x),
    }

    println!("at the end: x = {:?}, y = {y}", x);
}

这段代码现在将打印Default case, x = Some(5)。第二个匹配arm 中的模式并没有引入一个新的变量y来遮蔽外层y,这意味着我们可以在匹配守卫中使用外层y。我们指定Some(n),而不是将模式指定为Some(y),这将掩盖外层的y。这将创建一个新变量n,它不会遮蔽任何东西,因为在match之外没有n个变量。

匹配守卫if n == y不是一个模式,因此不会引入新的变量。这个y是外层的y,而不是一个新的遮蔽y,我们可以通过比较ny来寻找一个与外层y相同的值。

你也可以在匹配守卫中使用or操作符|来指定多个模式;match guard条件将应用于所有模式。示例18-28显示了组合使用|和匹配守卫的模式时的优先级。这个例子的重要部分是if y match守卫应用于4、5和6,尽管它看起来像是if y只应用于6

    let x = 4;
    let y = false;

    match x {
        4 | 5 | 6 if y => println!("yes"),
        _ => println!("no"),
    }

匹配条件指出,只有当x的值等于4、5或6并且ytrue时,arm 才匹配。当这段代码运行时,第一个 arm 的模式匹配,因为x4,但如果y为假,则不匹配守卫,因此第一个 arm 不被选择。代码继续移动到匹配的第二个 arm ,这个程序输出no。原因是if条件适用于整个模式4 | 5 | 6,而不仅仅适用于最后一个值6。换句话说,匹配守卫相对于模式的优先级是这样的:

(4 | 5 | 6) if y => ...

而不是这样:

4 | 5 | (6 if y) => ...

运行代码后,优先级行为很明显:如果匹配守卫仅应用于使用|操作符指定的值列表中的最后一个值,则arm将匹配,程序将打印yes

18.3.8 @ 绑定

at操作符@允许我们在测试模式匹配的值的同时创建一个保存值的变量。在示例18-29中,我们想测试Message::Hello id字段是否在3..=7的范围内。我们还希望将该值绑定到变量id_variable,以便在与arm相关的代码中使用它。我们可以将此变量命名为id,与字段相同,但在本例中,我们将使用不同的名称。

    enum Message {
        Hello { id: i32 },
    }

    let msg = Message::Hello { id: 5 };

    match msg {
        Message::Hello {
            id: id_variable @ 3..=7,
        } => println!("Found an id in range: {}", id_variable),
        Message::Hello { id: 10..=12 } => {
            println!("Found an id in another range")
        }
        Message::Hello { id } => println!("Found some other id: {}", id),
    }

这个例子将打印Found an id in range: 5通过在范围3..=7之前指定id_variable @,我们捕获与范围匹配的任何值,同时测试该值是否与范围模式匹配

在第二个 arm 中,我们只在模式中指定了一个范围,与arm关联的代码没有包含id字段实际值的变量。id字段的值可以是10、11或12,但是使用该模式的代码不知道它是哪一个。模式代码不能使用id字段的值,因为我们还没有在变量中保存id值。

在最后一个 arm 中,我们指定了一个没有范围的变量,我们确实在 arm 的代码中有一个名为id的变量的可用值。原因是我们使用了struct字段简写语法。但是我们没有对这个 arm 的id字段中的值应用任何测试,就像我们对前两个 arm 所做的那样:任何值都会匹配这个模式。

使用@让我们可以测试一个值,并将其保存在一个模式中的变量中。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值