Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

18.3 模式(匹配)的语法

18.3.1. 匹配字面值

模式可以直接匹配字面值。看个例子:

#![allow(unused)]
fn main() {
let x = 1;

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

这段代码会打印 one,因为 x 中的值是 1。当你希望代码针对某个特定值采取行动时,这种语法非常有用。

18.3.2. 匹配命名变量

命名变量是可以匹配任意值的无可辩驳模式。看个例子:

#![allow(unused)]
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}");
}

这个例子的逻辑很简单;关键在于这里出现了两个名为 y 的名字。它们互不相关,分别处于不同的作用域。let y = 10 中的 y 用来存储 10,而 Some(y) 中的 y 用来提取 Option 类型的 Some 变体所携带的数据。

match 中的执行逻辑如下:

  • 第一个分支中的模式与 x 的值不匹配,因此继续往下执行。

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

  • 如果 xNone 而不是 Some(5)——当然在这个例子里不可能发生——那么前两个分支中的模式都不会匹配,于是会匹配到 _。我们没有在通配符分支中引入 x 变量,所以表达式里的 x 仍然是未被遮蔽的外部 x。在那种假设情况下,match 会打印 Default case, x = None

输出:

Matched, y = 5
at the end: x = Some(5), y = 10

18.3.3. 多重模式

match 表达式里,可以使用管道符 | 语法(意思是)来匹配多种模式。看个例子:

#![allow(unused)]
fn main() {
let x = 1;

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

例子中的第一个分支会在 x 为 1 或 2 时匹配。

18.3.4. 使用 ..= 来匹配某个范围的值

看个例子:

#![allow(unused)]
fn main() {
let x = 5;

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

这个例子的第一个分支表示:当 x 是从 1 到 5(含两端)的任意值——也就是 1、2、3、4 或 5——时都会匹配。

因为 Rust 能够判断范围是否为空的唯一类型是 char 和数值类型,所以范围只允许用于数字或 char 值。看个例子:

#![allow(unused)]
fn main() {
let x = 'c';

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

这个例子的第一个分支匹配从 aj 的字符,第二个分支匹配从 kz 的字符。

18.3.5. 解构以分解值

我们可以使用模式来解构 structenum 和元组,从而引用这些类型值的不同部分。

解构 struct

看个例子:

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);
}
  • Point 结构体有两个字段 xy,类型都是 i32
  • 有一个名为 pPoint 实例,其 x 字段为 0,y 字段为 7。
  • 然后我们用模式解构 p,把 x 的值绑定到 a,把 y 的值绑定到 b

这么写还是有些冗长。如果把变量名 a 改成 x,把 b 改成 y,就可以简写成这样:

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

解构还可以灵活地使用。看个例子:

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})");
        }
    }
}
  • 第一个分支要求 x 字段为任意值,y 字段为 0。
  • 第二个分支要求 x 字段为 0,y 字段为任意值。
  • 第三个分支对 xy 的值没有任何限制。

解构 enum

看个例子:

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

这段代码会打印 Change the color to red 0, green 160, and blue 255

解构嵌套的 structenum

看个例子:

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

MessageChangeColor 变体所携带的数据就是 Color 枚举。使用 match 表达式时,一层一层匹配即可。在 match 的前两个分支中,外层都是 ChangeColor 变体,内层分别对应 Color 的两个变体;里面的值都可以通过变量提取出来。

解构 struct 和元组

看个例子:

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

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

main 中模式的外层是一个有两个元素的元组:

  • 第一个元素本身又是一个有两个元素的元组。
  • 第二个元素是一个 Point 结构体。

在模式中忽略值

有几种方式可以在模式中忽略整个值或部分值:

  • _:忽略整个值
  • _ 配合其他模式:忽略部分值
  • 使用以 _ 开头的名称
  • ..:忽略值的剩余部分

使用 _ 来忽略整个值

看个例子:

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

使用嵌套的 _ 来忽略值的一部分

看个例子:

#![allow(unused)]
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,然后打印 setting is Some(5)。在第一个分支中,我们不需要匹配或使用 Some 变体里的值,但我们确实需要确认 setting_valuenew_setting_value 都是 Some 变体。这就是忽略值的一部分的含义。

第二个分支表示在所有其他情况下——如果 setting_valuenew_setting_valueNone——就把 new_setting_value 赋给 setting_value。这就是把 _ 与其他模式配合使用来忽略值的例子。

我们还可以在同一个模式的多个位置使用下划线,来忽略特定值。看个例子:

#![allow(unused)]
fn main() {
let numbers = (2, 4, 8, 16, 32);

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

这里忽略了元组的第 2 个和第 4 个元素。这段代码会打印 Some numbers: 2, 8, 32,而值 4 和 16 会被忽略。

使用以 _ 开头的名称来忽略未使用的变量

看个例子:

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

正常情况下,如果你创建了变量却没有使用它,Rust 编译器会发出警告。这里 _xy 都没有被使用,但对 y 会有警告。那是因为 _x_ 开头,告诉编译器这是一个临时变量。

请注意:只使用 _ 和使用以下划线开头的名称之间存在细微差别。语法 _x 仍然会把值绑定到变量,而 _ 根本不会绑定任何东西。看个例子:

#![allow(unused)]
fn main() {
let s = Some(String::from("Hello!"));

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

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

我们会收到一个错误,因为 s 的值仍然会被移动到 _s 中,这会阻止我们打印 s

在这种情况下,应该使用 _ 来避免绑定值:

#![allow(unused)]
fn main() {
let s = Some(String::from("Hello!"));

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

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

使用 .. 来忽略值的剩余部分

看个例子:

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

fn main() {
    let origin = Point { x: 0, y: 0, z: 0 };

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

使用 match 匹配时,我们只需要 x 字段,所以模式只写 x,其余部分用 .. 覆盖。

这么使用 .. 也是可以的:

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

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

这只会取第一个和最后一个值,并忽略其余部分。

这么写 .. 是不行的:

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

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

这里前面有 ..,后面也有 ..,而我们想要中间的元素。但具体是哪个元素呢?这么写时,编译器不知道 .. 应该跳过多少个元素,因此也不知道 second 指的是哪个元素。

输出:

$ cargo run
   Compiling patterns v0.1.0 (/tmp/ch18-refresh/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

18.3.6. 使用 match guards 来提供额外条件

match guardsmatch 守卫)是 match 分支模式后面附加的 if 条件。分支要匹配,这个条件也必须满足。match guards 适用于比单纯模式更复杂的场景。

看个例子:

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 => (),
    }
}

match 的第一个分支中,Some(x) 是模式,而 if x % 2 == 0 就是 match guard,它要求 Some 所携带的数据能被 2 整除。

无法在模式本身中表达 if x % 2 == 0 这个条件,因此 match guards 让我们能够表达这种逻辑。这种额外表达能力的缺点是:一旦涉及 match guard,编译器就不会再尝试检查穷尽性。

看第二个例子:

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

这段代码现在会打印 Default case, x = Some(5)

match 守卫 if n == y 不是模式,因此不会引入新变量。这个 y 是外部的 y(值为 10),而不是新的遮蔽变量 y。我们可以通过比较,找出与外部 y 具有相同值的那些 n

看第三个例子:

#![allow(unused)]
fn main() {
let x = 4;
let y = false;

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

这个例子把 match 守卫与多重模式一起使用。

匹配条件规定:只有当 x 为 4、5 或 6,并且 ytrue 时,该分支才会匹配。运行这段代码时,x 是 4,但 match guard 中的 yfalse,所以第一个分支不会执行,第二个分支会打印 no

这里需要注意的是模式相对于 match 守卫的优先级。它应该是:

#![allow(unused)]
fn main() {
(4 | 5 | 6) if y => ...
}

而不是:

#![allow(unused)]
fn main() {
4 | 5 | (6 if y) => ...
}

18.3.7. @ 绑定

@ 符号让我们可以创建一个变量,该变量可以在测试某个值是否与模式匹配的同时保存该值。

看个例子:

enum Message {
    Hello { id: i32 },
}

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

在这个 match 的第一个分支中,id 字段的值被绑定到 id_variable,同时还会检查它是否落在从 3 到 7(含两端)的闭区间内。