模式语法

在本节中,我们收集了模式中所有有效的语法,并讨论了原因和 何时您可能想要使用每一个。

匹配文本

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

fn main() {
    let x = 1;

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

此代码打印one因为x为 1。此语法非常有用 当你希望代码在获得特定具体 价值。

匹配命名变量

命名变量是匹配任何值的无可辩驳的模式,我们已经使用了 他们在书中多次出现。但是,当您使用 命名变量match表达 式。因为match启动新范围, 变量声明为模式的一部分,在match表达式将 shadow 那些在match构造,就像 替换为所有变量。在示例 18-11 中,我们声明了一个名为x使用 价值Some(5)和一个变量y使用值10.然后,我们创建一个matchvalue 上的 expressionx.查看 match arms 和println!,并尝试弄清楚代码之前将打印什么 运行此代码或进一步阅读。

文件名: src/main.rs

fn main() {
    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 = {x:?}, y = {y}");
}

示例 18-11:一个match表达式替换为 引入 shadowed 变量y

我们来看看当match表达式运行。模式 在第一个匹配项 ARM 中,与定义的x,因此代码 继续。

第二个匹配臂中的模式引入了一个名为y那 将匹配Some价值。因为我们在一个新的范围内 这match表达式,这是一个新的y变量,而不是y我们在 以值 10 开头。这个新的ybinding 将匹配任何值 在Some,这就是我们在x.因此,这个新的y绑定到 的Somex.该值为5,因此 该 arm 执行并打印Matched, y = 5.

如果x曾是Nonevalue 而不是Some(5)中,第一个 两个 Arms 不匹配,因此该值将与 强调。我们没有引入x变量在 underscore arm,因此x在表达式中仍然是外部的x那没有 被遮蔽。在这个假设的情况下,match将打印Default case, x = None.

matchexpression 完成,则其范围结束,则 内部y.最后println!生产at the end: x = Some(5), y = 10.

要创建match表达式,该表达式比较外部xy,而不是引入阴影变量,我们需要使用 match guard 条件。我们将在后面的“Extra Conditionals with Match Guards“ 部分。

多种模式

match表达式中,您可以使用|语法 这是 pattern operator。例如,在下面的代码中,我们将 的值x针对匹配分支,其中第一个分支具有 OR 选项, 这意味着如果x匹配该 Arm 中的任一值,则 ARM 的代码将运行:

fn main() {
    let x = 1;

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

此代码打印one or two.

将值范围与..=

..=syntax 允许我们匹配一个包含的值范围。在 以下代码中,当模式与给定 range,该 arm 将执行:

fn main() {
    let x = 5;

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

如果x是 1、2、3、4 或 5,则第一个分支将匹配。此语法更 比使用|运算符来表示 同样的想法;如果我们要使用|我们必须指定1 | 2 | 3 | 4 | 5. 指定范围要短得多,特别是如果我们想匹配,比如说,任何 数字在 1 到 1,000 之间!

编译器在编译时检查范围是否不为空,并且由于 只有 Rust 可以判断范围是否为空的类型是char和 numeric 值,范围只允许与 numeric 或char值。

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

fn main() {
    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.

解构以分离值

我们还可以使用 patterns 来解构结构体、枚举和元组以使用 这些值的不同部分。让我们来看看每个值。

解构结构体

示例 18-12 显示了一个Pointstruct 中具有两个字段,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:将结构体的 fields 解构为 分隔变量

此代码创建变量abxy字段的p结构。此示例显示 模式中的变量不必与结构体的字段名称匹配。 但是,通常将变量名称与字段名称匹配以使其 更容易记住哪些变量来自哪些字段。正因为如此 常见用法,并且因为编写let Point { x: x, y: y } = p;包含一个 大量重复,Rust 有一个匹配结构体字段的模式的简写: 您只需列出 struct 字段的名称和创建的变量 将具有相同的名称。示例 18-13 的行为相同 方式与示例 18-12 中的代码一样,但是在letpattern 为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:使用 struct 解构 struct 字段 字段速记

此代码创建变量xy匹配xy领域 的p变量。结果是变量xy包含 值p结构。

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

在示例 18-14 中,我们有一个match分隔Point值 分为三种情况:直接位于xaxis (当y = 0)、在y轴 (x = 0),或者两者都不是。

文件名: src/main.rs

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

示例 18-14:解构和匹配 Literals 值 在一个模式中

第一个分支将匹配位于x轴,方法是指定 这y如果 field 的值与文本匹配,则 field 匹配0.模式依然 创建一个x变量,我们可以在此 ARM 的代码中使用。

同样,第二个分支与y轴,方法是指定 这xfield 如果其值为0并创建一个变量y对于 的值y田。第三个分支没有指定任何文本,因此它 匹配任何其他Point并为xy领域。

在此示例中,值p凭借x包含 0,因此此代码将打印On the y axis at 7.

请记住,一个match表达式在找到 第一个匹配模式,因此即使Point { x: 0, y: 0}位于x轴 和yaxis 的 Axis 中,此代码只会打印On the x axis at 0.

解构枚举

我们在本书中解构了枚举(例如,第 6 章中的示例 6-5), 但尚未明确讨论解构枚举的模式 对应于定义存储在枚举中的数据的方式。作为 例如,在示例 18-15 中,我们使用了Messageenum 并写入 一个match替换为将解构每个内部值的模式。

文件名: src/main.rs

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 {x} and in the y direction {y}");
        }
        Message::Write(text) => {
            println!("Text message: {text}");
        }
        Message::ChangeColor(r, g, b) => {
            println!("Change the color to red {r}, green {g}, and blue {b}")
        }
    }
}

示例 18-15:解构持有 不同类型的值

此代码将打印Change the color to red 0, green 160, and blue 255.尝试 更改msg以查看来自其他 ARM 运行的代码。

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

对于类似结构体的枚举变体,例如Message::Move,我们可以使用 类似于我们指定用于匹配结构体的模式。在变体名称之后,我们 放置大括号,然后列出带有变量的字段,以便我们分开 此 Arm 的代码中使用的 pieces。这里我们使用简写形式为 我们在示例 18-13 中这样做了。

对于类似元组的枚举变体,如Message::Write,其中包含一个 元素和Message::ChangeColor,它包含一个包含三个元素的元组, pattern 类似于我们指定用于匹配 Tuples 的模式。的 模式中的变量必须与 Variety 中的元素数量匹配 匹配。

解构嵌套结构体和枚举

到目前为止,我们的例子都是匹配一级深的结构体或枚举, 但是 Matching 也适用于嵌套项目!例如,我们可以重构 示例 18-15 中的代码在ChangeColormessage 中,如示例 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 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表达式与Message::ChangeColorenum 变体,其中包含Color::Rgb变体;然后 模式绑定到三个内部i32值。第二个 arm 还会匹配Message::ChangeColorenum 变体,但内部枚举 比赛Color::Hsv相反。我们可以在一个matchexpression,即使涉及两个枚举。

解构结构和元组

我们可以以更复杂的方式混合、匹配和嵌套解构模式。 以下示例显示了一个复杂的解构,其中我们将结构体和 tuples 并解构所有 primitive 值:

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

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

这段代码让我们将复杂类型分解为它们的组成部分,以便我们可以使用 我们感兴趣的值。

使用 patterns 解构是使用值片段的便捷方式,例如 作为结构体中每个字段的值,彼此分开。

忽略模式中的值

您已经看到,忽略模式中的值有时很有用,例如 在match,以获得一个实际上并不重要的 Anything but 不考虑所有剩余的可能值。有一些 忽略模式中整个值或部分值的方法:使用模式(您已经见过),在另一个模式中使用模式, 使用以下划线开头的名称,或使用__..忽略剩余 值的一部分。让我们探讨一下如何以及为什么使用这些模式。

忽略 Entire Value 和_

我们使用下划线作为通配符模式,它将匹配除 not 绑定到值。这尤其有用,因为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,但 function body 不需要任何参数。你 然后避免收到有关未使用函数参数的编译器警告,因为 如果你用一个名字来代替。

忽略具有 Nested 的值的部分_

我们还可以在 inside another pattern 中使用来忽略值的一部分,因为 例如,当我们只想测试值的一部分,但不想使用 我们想要运行的相应代码中的其他部分。示例 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:?}");
}

示例 18-18:在 火柴Some变体,当我们不需要使用Some

此代码将打印Can't overwrite an existing customized value然后setting is Some(5).在第一个 match 分支中,我们不需要 match on 或使用 其中的Some变体,但我们确实需要测试该情况 什么时候setting_valuenew_setting_valueSome变体。在那个 case 时,我们打印不更改的原因setting_value,并且它不会得到 改变。

在所有其他情况下(如果setting_valuenew_setting_valueNone) 表示的 Pattern 表示的_new_setting_value成为setting_value.

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

fn main() {
    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,值 4 和 16 将为 忽视。

忽略未使用的变量,使其名称以_

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

文件名: src/main.rs

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

示例 18-20:以 下划线以避免收到未使用的变量警告

在这里,我们收到一条关于不使用变量的警告y,但是我们没有得到 关于不使用的警告_x.

请注意,仅使用和使用 name 之间存在细微差别 这以下划线开头。语法__xstill 将值绑定到 variable,而根本不绑定。要显示此 区别很重要,示例 18-21 会给我们一个错误。_

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

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

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

示例 18-21:以 下划线仍然绑定值,该值可能会获得值的所有权

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

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

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

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

示例 18-22:使用下划线不会绑定 价值

这段代码工作得很好,因为我们从不绑定s对任何东西;它不会移动。

忽略 Value 的剩余部分..

对于包含许多部分的值,我们可以使用..语法以使用特定的 部分并忽略其余部分,从而避免为每个部分列出下划线的需要 ignored 值。这..pattern 会忽略值中我们尚未忽略的部分 在 pattern 的其余部分显式匹配。在示例 18-23 中,我们有一个Point在三维空间中保存坐标的结构。在match表达式,我们只想对x协调和忽略 的yz领域。

fn main() {
    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:仅匹配 中的第一个和最后一个值 一个 Tuples 并忽略所有其他值

在此代码中,第一个和最后一个值与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:尝试使用..在一个模棱两可的 道路

当我们编译此示例时,我们会收到以下错误:

$ 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` (bin "patterns") due to 1 previous error

Rust 无法确定要忽略元组中的多少个值 在将值与second然后是 此后忽略。这段代码可能意味着我们想忽略2second4,然后忽略8,1632;或者我们想忽略的24second8,然后忽略1632;等等。 变量名称second对 Rust 来说没有什么特别的意义,所以我们得到了一个 编译器错误,因为使用..在两个地方,像这样是模棱两可的。

带有 Match Guard 的额外条件语句

火柴守卫是额外的ifcondition,在 一个matcharm,该 arm也必须与要选择的 arm 匹配。火柴守卫是 对于表达比单独的模式所允许的更复杂的想法很有用。

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

fn main() {
    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:向模式添加 match 守卫

此示例将打印The number 4 is even.什么时候num与 pattern 中,它会匹配,因为Some(4)比赛Some(x).然后 match 守卫检查 divideing 的余数是否xby 2 等于 0,因为它是,所以会选择第一个分支。

如果num一直Some(5)相反,第一臂中的火柴后卫会 都是 false,因为 5 除以 2 的余数是 1,而 1 不是 等于 0。然后 Rust 将转到第二个分支,这将匹配,因为 第二个臂没有匹配守卫,因此匹配任何Some变体。

没有办法表示if x % 2 == 0condition 的 Pattern 中,因此 match 守卫让我们能够表达这个逻辑。缺点 这种额外的表达性是编译器不会尝试检查 涉及 match guard 表达式时的穷举性。

在示例 18-11 中,我们提到我们可以使用 match 守卫来解决 pattern-shadowing 问题。回想一下,我们在 pattern 中matchexpression 而不是使用match.这个新变量意味着我们无法根据 outer 变量。示例 18-27 展示了如何使用 match 守卫来解决这个问题 问题。

文件名: 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 = {x:?}, y = {y}");
}

示例 18-27:使用 match 守卫测试相等性 替换为外部变量

此代码现在将打印Default case, x = Some(5).第二个 match arm 不会引入新变量y那会遮蔽外层y, 这意味着我们可以使用 externaly在火柴后卫中。而不是指定 pattern 设置为Some(y),这会遮蔽外部y,我们指定Some(n).这将创建一个新变量n那不会影子,因为 没有n变量match.

火柴后卫if n == y不是模式,因此不会引入 新变量。这y 外部的y而不是新的阴影y和 我们可以寻找一个与 outer 具有相同值的值y通过比较ny.

您还可以使用 or 运算符|在 match 守卫中指定多个 模式;match guard 条件将应用于所有模式。清单 18-28 显示了组合使用|带火柴 警卫。此示例的重要部分是if y比赛守卫 适用于4,5 6,即使它可能看起来像if y只 适用于6.

fn main() {
    let x = 4;
    let y = false;

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

示例 18-28:将多个模式与匹配项组合在一起 警卫

匹配条件声明,仅当x是 等于4,56 如果ytrue.当此代码运行时, pattern 匹配,因为x4,但 match 后卫if y为 false,因此不会选择第一个分支。代码移动到第二个分支 匹配,并且此程序会打印no.原因是if条件应用于整个模式4 | 5 | 6,不仅到最后一个值6.换句话说,match 守卫相对于 pattern 的优先级 的行为如下:

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

而不是这样:

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

运行代码后,优先行为很明显:如果 match 守卫 仅应用于使用|运算符,则 arm 将匹配,并且程序将打印yes.

@绑定

at 运算符让我们创建一个变量,该变量保存一个值 time 进行验证,因为我们要测试 pattern match 的该值。在示例 18-29 中,我们希望 以测试@Message::Hello id字段在范围3..=7.我们还 想要将值绑定到变量id_variable因此,我们可以在 与 ARM 关联的代码。我们可以将这个变量命名为id,与 field 中,但在此示例中,我们将使用不同的名称。

fn main() {
    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}"),
    }
}

示例 18-29:用于绑定到模式中的值 同时也对其进行了测试@

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

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

在最后一个分支中,我们指定了一个没有范围的变量,我们确实有 可在 Arm 代码中名为id.这 原因是我们使用了 struct field 速记语法。但我们没有 将任何测试应用于id字段,就像我们对 First Two Arms:任何值都将匹配此模式。

Using 让我们测试一个值并将其保存在一个模式中的变量中。@

总结

Rust 的模式在区分不同种类的 数据。当用于match表达式,Rust 会确保你的模式覆盖每个 可能的值,否则您的程序将无法编译。模式letstatements 和 函数参数使这些结构更有用,从而启用 将值解构为更小的部分,同时赋值给 变量。我们可以创建简单或复杂的模式来满足我们的需要。

接下来,在本书的倒数第二章,我们将看看一些高级 Rust 的各种功能的各个方面。

本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准