Rust 模式语法实践

RustRustBeginner
立即练习

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

💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版

简介

欢迎来到模式语法。本实验是《Rust 程序设计语言》的一部分。你可以在 LabEx 中练习 Rust 技能。

在本实验中,我们将讨论模式中的有效语法,并举例说明何时以及为何要使用每种语法。


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/MemorySafetyandManagementGroup(["Memory Safety and Management"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/BasicConceptsGroup -.-> rust/mutable_variables("Mutable Variables") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/DataTypesGroup -.-> rust/boolean_type("Boolean Type") rust/DataTypesGroup -.-> rust/string_type("String Type") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/MemorySafetyandManagementGroup -.-> rust/lifetime_specifiers("Lifetime Specifiers") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") subgraph Lab Skills rust/variable_declarations -.-> lab-100446{{"Rust 模式语法实践"}} rust/mutable_variables -.-> lab-100446{{"Rust 模式语法实践"}} rust/integer_types -.-> lab-100446{{"Rust 模式语法实践"}} rust/boolean_type -.-> lab-100446{{"Rust 模式语法实践"}} rust/string_type -.-> lab-100446{{"Rust 模式语法实践"}} rust/function_syntax -.-> lab-100446{{"Rust 模式语法实践"}} rust/expressions_statements -.-> lab-100446{{"Rust 模式语法实践"}} rust/lifetime_specifiers -.-> lab-100446{{"Rust 模式语法实践"}} rust/method_syntax -.-> lab-100446{{"Rust 模式语法实践"}} end

模式语法

在本节中,我们汇总了模式中所有有效的语法,并讨论你可能想要使用每种语法的原因和时机。

匹配字面量

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

文件名:src/main.rs

let x = 1;

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

这段代码会打印出 one,因为 x 中的值是 1。当你希望代码在获取到特定的具体值时执行某个操作时,这种语法很有用。

匹配命名变量

命名变量是不可反驳的模式,可匹配任何值,并且我们在本书中已经多次使用它们。但是,当你在 match 表达式中使用命名变量时,会有一个复杂的情况。因为 match 会开启一个新的作用域,在 match 表达式内部作为模式一部分声明的变量会遮蔽 match 结构外部具有相同名称的变量,所有变量都是如此。在清单18-11中,我们声明了一个值为 Some(5) 的名为 x 的变量和一个值为 10 的变量 y。然后我们对 x 的值创建一个 match 表达式。查看 match 分支中的模式以及最后的 println!,在运行这段代码或继续阅读之前,试着弄清楚这段代码会打印什么。

文件名:src/main.rs

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

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

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

清单18-11:一个 match 表达式,其中一个分支引入了一个被遮蔽的变量 y

让我们逐步分析 match 表达式运行时会发生什么。第一个 match 分支 [3] 中的模式与定义的 x 的值 [1] 不匹配,所以代码继续执行。

第二个 match 分支 [4] 中的模式引入了一个名为 y 的新变量,它将匹配 Some 值内的任何值。因为我们处于 match 表达式内部的一个新作用域中,这是一个新的 y 变量,而不是我们在开头声明的那个值为 10y [2]。这个新的 y 绑定将匹配 Some 内的任何值,这正是我们在 x 中所拥有的。因此,这个新的 y 绑定到 xSome 的内部值。那个值是 5,所以该分支的表达式会执行并打印 Matched, y = 5

如果 xNone 值而不是 Some(5),前两个分支中的模式将不匹配,所以值将匹配到下划线 [5]。我们在 _ 分支的模式中没有引入 x 变量,所以表达式中的 x 仍然是未被遮蔽的外部 x。在这个假设的情况下,match 将打印 Default case, x = None

match 表达式完成时,它的作用域结束,内部 y 的作用域也结束。最后的 println! [6] 会输出 at the end: x = Some(5), y = 10

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

多个模式

match 表达式中,你可以使用 | 语法来匹配多个模式,| 是模式的“或”运算符。例如,在以下代码中,我们将 x 的值与 match 分支进行匹配,第一个分支具有“或”选项,这意味着如果 x 的值与该分支中的任何一个值匹配,该分支的代码就会运行:

文件名:src/main.rs

let x = 1;

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

这段代码会打印出 one or two

使用..=匹配值的范围

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

文件名:src/main.rs

let x = 5;

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

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

编译器在编译时会检查范围是否为空,并且因为Rust能够判断范围是否为空的唯一类型是 char 和数值类型,所以范围只允许用于数值或 char 值。

这里有一个使用 char 值范围的示例:

文件名:src/main.rs

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-12展示了一个名为 Point 的结构体,它有两个字段 xy,我们可以使用带有 let 语句的模式来拆解它。

文件名:src/main.rs

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

清单18-12:将结构体的字段解构为单独的变量

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

文件名:src/main.rs

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

清单18-13:使用结构体字段简写形式解构结构体字段

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

我们还可以在结构体模式中使用字面量值进行解构,而不是为所有字段创建变量。这样做可以让我们在创建用于解构其他字段的变量时,测试某些字段是否具有特定值。

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

文件名:src/main.rs

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

清单18-14:在一个模式中解构并匹配字面量值

第一个分支将匹配任何位于 x 轴上的点,方法是指定如果 y 字段的值与字面量 0 匹配,则该字段匹配。该模式仍然会创建一个 x 变量,我们可以在该分支的代码中使用它。

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

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

请记住,match 表达式一旦找到第一个匹配的模式,就会停止检查其他分支,所以即使 Point { x: 0, y: 0} 既在 x 轴上又在 y 轴上,这段代码也只会打印 On the x axis at 0

解构枚举

在本书中我们已经对枚举进行了解构(例如,清单6-5),但我们尚未明确讨论解构枚举的模式与枚举中存储的数据的定义方式相对应。例如,在清单18-15中,我们使用清单6-2中的 Message 枚举,并编写一个 match 语句,其中的模式将解构每个内部值。

文件名:src/main.rs

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

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

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

清单18-15:解构包含不同类型值的枚举变体

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

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

对于类似结构体的枚举变体,比如 Message::Move [3],我们可以使用与匹配结构体时指定的模式类似的模式。在变体名称之后,我们放置花括号,然后列出带有变量的字段,这样我们就可以拆解这些部分以便在该分支的代码中使用。这里我们使用了与清单18-13中相同的简写形式。

对于类似元组的枚举变体,比如包含一个元素的元组的 Message::Write [4] 和包含三个元素的元组的 Message::ChangeColor [5],模式与我们指定用于匹配元组的模式类似。模式中的变量数量必须与我们正在匹配的变体中的元素数量相匹配。

解构嵌套结构体和枚举

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

文件名:src/main.rs

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 color to red {r}, green {g}, and blue {b}"
        ),
        Message::ChangeColor(Color::Hsv(h, s, v)) => println!(
            "Change color to hue {h}, saturation {s}, value {v}"
        ),
        _ => (),
    }
}

清单18-16:对嵌套枚举进行匹配

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

解构结构体和元组

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

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

这段代码让我们能够将复杂类型分解为其组成部分,这样我们就可以分别使用我们感兴趣的值。

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

忽略模式中的值

你已经看到,在模式中忽略某些值有时是很有用的,比如在 match 的最后一个分支中,用于创建一个通配分支,它实际上不做任何事情,但能处理所有剩余的可能值。有几种方法可以在模式中忽略整个值或值的部分:使用 _ 模式(你已经见过)、在另一个模式中使用 _ 模式、使用以下划线开头的名称,或者使用 .. 来忽略值的其余部分。让我们探讨一下如何以及为何使用这些模式中的每一种。

使用 _ 忽略整个值

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

文件名:src/main.rs

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

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

清单18-17:在函数签名中使用 _

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

在大多数情况下,当你不再需要某个特定的函数参数时,你会更改签名,使其不包括未使用的参数。在某些情况下,忽略函数参数可能特别有用,例如,当你实现一个 trait 时,你需要特定的类型签名,但实现中的函数体不需要其中一个参数。这样你就可以避免收到关于未使用函数参数的编译器警告,而如果你使用一个名称,就会收到这样的警告。

在嵌套模式中忽略值的部分

我们还可以在另一个模式中使用 _ 来仅忽略值的一部分,例如,当我们只想测试值的一部分,而在要运行的相应代码中不需要其他部分时。清单18-18展示了负责管理设置值的代码。业务需求是不允许用户覆盖现有设置的定制,但可以取消设置,如果当前未设置则可以赋予其一个值。

文件名:src/main.rs

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

清单18-18:在匹配 Some 变体的模式中使用下划线,当我们不需要使用 Some 内部的值时

这段代码将打印 Can't overwrite an existing customized value,然后打印 setting is Some(5)。在第一个匹配分支中,我们不需要匹配或使用任一 Some 变体内部的值,但我们确实需要测试 setting_valuenew_setting_value 都是 Some 变体的情况。在这种情况下,我们打印不更改 setting_value 的原因,并且它不会被更改。

在第二个分支中由 _ 模式表示的所有其他情况(即 setting_valuenew_setting_valueNone)下,我们希望允许 new_setting_value 成为 setting_value

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

文件名:src/main.rs

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

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

清单18-19:忽略元组的多个部分

这段代码将打印 Some numbers: 2, 8, 32,并且值 416 将被忽略。

通过以下划线开头来创建未使用的变量

如果你创建了一个变量但在任何地方都不使用它,Rust通常会发出警告,因为未使用的变量可能是一个错误。然而,有时能够创建一个你尚未使用的变量是很有用的,比如在你进行原型设计或刚开始一个项目的时候。在这种情况下,你可以通过以下划线开头来告诉Rust不要因为未使用该变量而警告你。在清单18-20中,我们创建了两个未使用的变量,但当我们编译这段代码时,我们应该只会收到关于其中一个变量的警告。

文件名:src/main.rs

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

清单18-20:以下划线开头来创建变量以避免收到未使用变量的警告

在这里,我们收到了关于未使用变量 y 的警告,但没有收到关于未使用 _x 的警告。

请注意,仅使用 _ 和以下划线开头的名称之间存在细微差别。语法 _x 仍然会将值绑定到变量,而 _ 根本不会绑定。为了展示这种区别很重要的情况,清单18-21会给我们一个错误。

文件名:src/main.rs

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

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

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

清单18-21:以下划线开头的未使用变量仍然会绑定值,这可能会获取该值的所有权。

我们会收到一个错误,因为 s 的值仍然会被移动到 _s 中,这会阻止我们再次使用 s。然而,单独使用下划线永远不会绑定到值。清单18-22会编译通过且没有任何错误,因为 s 不会被移动到 _ 中。

文件名:src/main.rs

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

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

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

清单18-22:使用下划线不会绑定值。

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

使用 .. 忽略值的其余部分

对于具有多个部分的值,我们可以使用 .. 语法来使用特定部分并忽略其余部分,这样就无需为每个要忽略的值列出下划线。.. 模式会忽略我们在模式的其余部分未明确匹配的任何值的部分。在清单18-23中,我们有一个 Point 结构体,它表示三维空间中的一个坐标。在 match 表达式中,我们只想对 x 坐标进行操作,并忽略 yz 字段中的值。

文件名:src/main.rs

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

清单18-23:通过使用 .. 忽略 Point 结构体中除 x 之外的所有字段

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

.. 语法会扩展到所需的任意多个值。清单18-24展示了如何在元组中使用 ..

文件名:src/main.rs

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

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

清单18-24:仅匹配元组中的第一个和最后一个值,并忽略所有其他值

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

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

文件名:src/main.rs

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

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

清单18-25:以不明确的方式尝试使用 ..

当我们编译这个示例时,会得到以下错误:

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

Rust无法确定在将一个值与 second 匹配之前要忽略元组中的多少个值,以及在那之后还要忽略多少个值。这段代码可能意味着我们想忽略 2,将 second 绑定到 4,然后忽略 81632;或者我们想忽略 24,将 second 绑定到 8,然后忽略 1632;等等。变量名 second 对Rust来说没有什么特殊含义,所以我们会得到一个编译器错误,因为像这样在两个地方使用 .. 是不明确的。

使用匹配守卫进行额外条件判断

匹配守卫(match guard)是在 match 分支的模式之后指定的一个额外的 if 条件,只有当该条件也匹配时,这个分支才会被选中。匹配守卫对于表达比单独一个模式更复杂的想法很有用。

该条件可以使用在模式中创建的变量。清单18-26展示了一个 match,其中第一个分支的模式是 Some(x),并且还有一个匹配守卫 if x % 2 == 0(如果数字是偶数,该条件将为 true)。

文件名:src/main.rs

let num = Some(4);

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

清单18-26:给模式添加匹配守卫

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

如果 numSome(5),那么第一个分支中的匹配守卫将为 false,因为5除以2的余数是1,不等于0。然后Rust会转到第二个分支,该分支会匹配,因为第二个分支没有匹配守卫,因此可以匹配任何 Some 变体。

无法在模式中表达 if x % 2 == 0 这个条件,所以匹配守卫让我们有能力表达这个逻辑。这种额外表达能力的缺点是,当涉及到匹配守卫表达式时,编译器不会尝试检查是否穷举。

在清单18-11中,我们提到可以使用匹配守卫来解决模式遮蔽问题。回想一下,我们在 match 表达式的模式中创建了一个新变量,而不是使用 match 外部的变量。那个新变量意味着我们无法针对外部变量的值进行测试。清单18-27展示了如何使用匹配守卫来解决这个问题。

文件名:src/main.rs

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

清单18-27:使用匹配守卫来测试与外部变量是否相等

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

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

你还可以在匹配守卫中使用逻辑或运算符 | 来指定多个模式;匹配守卫条件将应用于所有模式。清单18-28展示了在将使用 | 的模式与匹配守卫组合时的优先级。这个示例的重点是,if y 匹配守卫适用于 456,尽管看起来 if y 只适用于 6

文件名:src/main.rs

let x = 4;
let y = false;

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

清单18-28:将多个模式与匹配守卫组合

匹配条件表明,只有当 x 的值等于 456 并且 ytrue 时,该分支才匹配。当这段代码运行时,第一个分支的模式匹配,因为 x4,但匹配守卫 if yfalse,所以不选择第一个分支。代码继续执行到第二个分支,该分支确实匹配,并且这个程序打印 no。原因是 if 条件适用于整个模式 4 | 5 | 6,而不仅仅是最后一个值 6。换句话说,匹配守卫相对于模式的优先级行为如下:

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

而不是这样:

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

运行代码后,优先级行为就很明显了:如果匹配守卫仅应用于使用 | 运算符指定的值列表中的最后一个值,那么该分支就会匹配,程序就会打印 yes

@ 绑定

at 运算符 @ 让我们在测试值是否匹配某个模式的同时,创建一个持有该值的变量。在清单18-29中,我们想要测试 Message::Helloid 字段是否在 3..=7 范围内。我们还想将该值绑定到变量 id_variable,以便在与该分支关联的代码中使用它。我们可以将这个变量命名为 id,与字段名相同,但在这个示例中我们将使用不同的名称。

文件名:src/main.rs

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!("Some other id: {id}"),
}

清单18-29:使用 @ 在模式中绑定值的同时进行测试

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

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

在最后一个分支中,我们指定了一个没有范围的变量,我们确实有一个名为 id 的变量,其值可在该分支的代码中使用。原因是我们使用了结构体字段简写语法。但在这个分支中,我们没有像前两个分支那样对 id 字段的值进行任何测试:任何值都将匹配这个模式。

使用 @ 让我们能够在一个模式中测试一个值并将其保存在一个变量中。

总结

恭喜你!你已经完成了模式语法实验。你可以在LabEx中练习更多实验来提升你的技能。