Rust Match全模式列表

TrumanWong
1/9/2025
TrumanWong

匹配字面量

fn main() {
    let x = 1;
    match x {
        1 => println!("one"),
        2 => println!("two"),
        3 => println!("three"),
        _ => {}
    }
}

因为x的值为1,所以最终结果输出为one。如果希望代码获得特定的具体值,那么这种语法很有用。

匹配命名变量

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

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

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

运行后得到结果:

Matched, y = 1
at the end: x = Some(1), y = 2

第一个匹配分支的模式并不匹配 x 中定义的值,所以代码继续执行。

第二个匹配分支中的模式引入了一个新变量 y,它会匹配任何 Some 中的值。因为这里的 ymatch 表达式的作用域中,而不是之前 main 作用域中,所以这是一个新变量,不是开头声明为值 2的那个 y。这个新的 y 绑定会匹配任何 Some 中的值,在这里是 x 中的值。因此这个 y 绑定了 xSome 内部的值。这个值是 5,所以这个分支的表达式将会执行并打印出 Matched,y = 1

如果 x 的值是 None 而不是 Some(1),前两个分支的模式不会匹配,所以会匹配模式 _。这个分支的模式中没有引入变量 x,所以此时表达式中的 x 会是外部没有被遮蔽的 x,也就是 None

一旦 match 表达式执行完毕,其作用域也就结束了,同理内部 y 的作用域也结束了。最后的 println! 会打印 at the end: x = Some(1), y = 2

如果不想引入变量遮蔽,可以使用另一个变量名而非 y,或者使用匹配守卫match guard的方式

单分支模式

match 表达式中,可以使用 | 语法匹配多个模式,它代表 的意思。如下示例将 a 的值与匹配分支相比较,第一个分支有 选项,意味着如果 a 的值匹配此分支的任何一个模式,它就会运行:

fn main() {
    let a = 1;

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

通过序列..=匹配值的范围

..= 语法允许你匹配一个闭区间序列内的值。在如下示例中,当模式匹配任何在此序列内的值时,该分支会执行:

fn main() {
    let a = 5;
    match a {
        1..=5 => println!("1..5"),
        _ => println!("default"),
    }
}

如果 a 是 1、2、3、4 或 5,第一个分支就会匹配。这相比使用 | 运算符表达相同的意思更为方便;相比 1..=5,使用 | 则不得不指定 1 | 2 | 3 | 4 | 5 这五个值,而使用 ..= 指定序列就简短的多,比如希望匹配比如从 1 到 1000 的数字的时候!

序列只允许用于数字或字符类型,原因是:它们可以连续,同时编译器在编译期可以检查该序列是否为空,字符和数字值是 Rust 中仅有的可以用于判断是否为空的类型。

以下是一个使用字符类型序列的例子:

fn main() {
    let x = 'c';

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

'c' 位于第一个模式的序列内,所以会打印出 early ASCII letter

结构并分解值

也可以使用模式来解构结构体、枚举、元组、数组和引用。

解构结构体

代码展示了如何用 let 解构一个带有两个字段 xy 的结构体 Point

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

fn main() {
    let p = Point { x: 1, y: 2 };
    let Point {x: a, y: b} = p;
    assert_eq!(a, 1);
    assert_eq!(b, 2);
}

这段代码创建了变量 ab 来匹配结构体 p 中的 xy 字段,这个例子展示了模式中的变量名不必与结构体中的字段名一致。不过通常希望变量名与字段名一致以便于理解变量来自于哪些字段。

因为变量名匹配字段名是常见的,同时因为 let Point { x: x, y: y } = p;xy 重复了,所以对于匹配结构体字段的模式存在简写:只需列出结构体字段的名称,则模式创建的变量会有相同的名称。下例与上例有着相同行为的代码,不过 let 模式创建的变量为 xy 而不是 ab

fn main() {
    let p = Point { x: 1, y: 2 };
    let Point { x, y } = p;
    assert_eq!(a, 1);
    assert_eq!(b, 2);
}

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

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

以下示例展示了固定某个字段的匹配方式:

fn main() {
    let p = Point { x: 1, y: 2 };

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

首先是 match 第一个分支,指定匹配 y1Point; 然后第二个分支在第一个分支之后,匹配 x1Point; 最后一个分支匹配命名变量。

在这个例子中,值 p 因为其 x 为1而匹配第二个分支,因此会打印出 On the y axis at 2

结构枚举

下面代码以 Message 枚举为例,编写一个 match 使用模式解构每一个内部值:

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

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

模式匹配一样要类型相同,因此匹配 Message::Move{1,2} 这样的枚举值,就必须要用 Message::Move{x,y} 这样的同类型模式才行。

这段代码会打印出 Change the color to red 0, green 160, and blue 255。尝试改变 msg 的值来观察其他分支代码的运行。

对于像 Message::Quit 这样没有任何数据的枚举成员,不能进一步解构其值。只能匹配其字面值 Message::Quit,因此模式中没有任何变量。

对于另外两个枚举成员,就用相同类型的模式去匹配出对应的值即可。

结构嵌套的结构体和枚举

目前为止,所有的例子都只匹配了深度为一级的结构体或枚举。 match 也可以匹配嵌套的项。

例如使用下面的代码来同时支持 RGBHSV 色彩模式:

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::Rgb(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 第一个分支的模式匹配一个 Message::ChangeColor 枚举成员,该枚举成员又包含了一个 Color::Rgb 的枚举成员,最终绑定了 3 个内部的 i32 值。第二个模式匹配一个 Message::ChangeColor 枚举成员,该枚举成员又包含了一个 Color::Hsv 的枚举成员,最终绑定了 3 个内部的 i32 值。

解构结构体和元组

我们可以用复杂的方式来混合、匹配和嵌套解构模式。如下是一个复杂结构体的例子,其中结构体和元组嵌套在元组中,并将所有的原始类型解构出来:

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

fn main() {
    let ((feet, inches), Point { x, y }) = ((1, 2), Point { x: 3, y: 4 });
    println!("feet: {}, inches: {}, x: {}, y: {}", feet, inches, x, y);
}

这种将复杂类型分解匹配的方式,可以让我们得到感兴趣的某个值。

解构数组

对于数组,我们可以用类似元组的方式解构,分为两种情况:

定长数组:

fn main() {
    let arr:[u32; 2] = [1, 2];
    let [x, y] = arr;
    assert_eq!(x, 1);
    assert_eq!(y, 2);
}

不定长数组:

fn main() {
    let arr: &[u32] = &[1, 2, 3, 4, 5];
    if let [x, ..] = arr {
        assert_eq!(x, &1);
    }

    if let &[.., y] = arr {
        assert_eq!(y, 5);
    }

    let arr: &[u32] = &[];
    assert!(matches!(arr, [..]));
    assert!(!matches!(arr, [x, ..]));
}

忽略模式中的值

有时忽略模式中的一些值是很有用的,比如在 match 中的最后一个分支使用 _ 模式匹配所有剩余的值。 你也可以在另一个模式中使用 _ 模式,使用一个以下划线开始的名称,或者使用 .. 忽略所剩部分的值。

使用_忽略整个值

虽然 _ 模式作为 match 表达式最后的分支特别有用,但是它的作用还不限于此。例如可以将其用于函数参数中:

fn foo(_: i32, y: i32) {
    println!("{}", y);
}

fn main() {
    foo(1, 2);
}

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

大部分情况当你不再需要特定函数参数时,最好修改签名不再包含无用的参数。在一些情况下忽略函数参数会变得特别有用,比如实现特征时,当你需要特定类型签名但是函数实现并不需要某个参数时。此时编译器就不会警告说存在未使用的函数参数,就跟使用命名参数一样。

使用嵌套的_忽略部分值

可以在一个模式内部使用 _ 忽略部分值:

fn main() {
    let mut val = Some(1);
    let new_val = Some(2);
    match (val, new_val) {
        (Some(_), Some(_)) => {
            println!("Some");
        },
        _ => {
            val = new_val;
        }
    }
    println!("{:?}",val);
}

这段代码会打印出 Some 接着是 Some(1)

第一个匹配分支,我们不关心里面的值,只关心元组中两个元素的类型,因此对于 Some 中的值,直接进行忽略。 剩下的形如 (Some(_),None)(None, Some(_)), (None,None) 形式,都由第二个分支 _ 进行分配。

还可以在一个模式中的多处使用下划线来忽略特定值,如下所示,这里忽略了一个五元元组中的第二和第四个值:

fn main() {
    let numbers = (1, 2, 3, 4, 5);
    match numbers {
        (first, _, thrid, _, last) => {
            println!("Some numbers: {}, {}, {}", first, thrid, last);
        }
    }
}

模式匹配一定要类型相同,因此匹配 numbers 元组的模式,也必须有五个值(元组中元素的数量也属于元组类型的一部分)。

这会打印出 Some numbers: 1, 3, 5, 值 2 和 4 会被忽略。

使用下划线开头忽略未使用的变量

如果你创建了一个变量却不在任何地方使用它,Rust 通常会给你一个警告,因为这可能会是个 bug。但是有时创建一个不会被使用的变量是有用的,比如你正在设计原型或刚刚开始一个项目。这时你希望告诉 Rust 不要警告未使用的变量,为此可以用下划线作为变量名的开头:

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

这里得到了警告说未使用变量 y,至于 x 则没有警告。

注意, 只使用 _ 和使用以下划线开头的名称有些微妙的不同:比如 _x 仍会将值绑定到变量,而 _ 则完全不会绑定

fn main() {
    let s = Some(String::from("Truman"));
    if let Some(_s) = s {
        println!("{:?}", _s);
    }
    println!("{:?}", s);
}

s 是一个拥有所有权的动态字符串,在上面代码中,我们会得到一个错误,因为 s 的值会被转移给 _s,在 println! 中再次使用 s 会报错:

$ cargo run
...
error[E0382]: borrow of partially moved value: `s`                                                                             
 --> src/main.rs:6:22
  |
3 |     if let Some(_s) = s {
  |                 -- value partially moved here
...
6 |     println!("{:?}", s);
  |                      ^ value borrowed here after partial move
  |
  = note: partial move occurs because value has type `String`, which does not implement the `Copy` trait
  = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
...

只使用下划线本身,则并不会绑定值,因为 s 没有被移动进 _

fn main() {
    let s = Some(String::from("Truman"));
    if let Some(_) = s {
        println!("found a string");
    }
    println!("{:?}", s);
}

用..忽略剩余值

对于有多个部分的值,可以使用 .. 语法来只使用部分值而忽略其它值,这样也不用再为每一个被忽略的值都单独列出下划线。.. 模式会忽略模式中剩余的任何没有显式匹配的值部分。

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

这里列出了 x 值,接着使用了 .. 模式来忽略其它字段,这样的写法要比一一列出其它字段,然后用 _ 忽略简洁的多。

还可以用 .. 来忽略元组中间的某些值:

fn main() {
    let numbers = (1, 2, 3, 4, 5);
    match numbers { 
        (first, .., last) => {
            println!("Some numbers: {}, {}", first, last);
        },
    }
}

这里用 firstlast 来匹配第一个和最后一个值。.. 将匹配并忽略中间的所有值。

然而使用 .. 必须是无歧义的。如果期望匹配和忽略的值是不明确的,Rust 会报错。下面代码展示了一个带有歧义的 .. 例子,因此不能编译:

fn main() {
    let numbers = (1, 2, 3, 4, 5);
    match numbers {
        (.., second, ..) => {
            println!("Some numbers: {}, {}", second);
        },
    }
}

如果编译上面的例子,会得到下面的错误:

$ cargo run
...
error: 2 positional arguments in format string, but there is 1 argument                                                        
 --> src/main.rs:5:37
  |
5 |             println!("Some numbers: {}, {}", second);
  |                                     ^^  ^^   ------

error: `..` can only be used once per tuple pattern
 --> src/main.rs:4:22
  |
4 |         (.., second, ..) => {
  |          --          ^^ can only be used once per tuple pattern
  |          |
  |          previously used here
...

Rust 无法判断,second 应该匹配 numbers 中的第几个元素,因此这里使用两个 .. 模式,是有很大歧义的!

匹配守卫提供的额外条件

匹配守卫match guard)是一个位于 match 分支模式之后的额外 if 条件,它能为分支模式提供更进一步的匹配条件。

这个条件可以使用模式中创建的变量:

fn main() {
    let num = Some(1);
    match num {
        Some(x) if x < 3 => println!("less than three, {}", x),
        Some(x) => println!("{}", x),
        None => (),
    }
}

这个例子会打印出 less than three, 1。当 num 与模式中第一个分支匹配时,Some(1) 可以与 Some(x) 匹配,接着匹配守卫检查 x 值是否小于 3,因为 1 小于 3,所以第一个分支被选择。

相反如果 numSome(10),因为 10 不小于 3 ,所以第一个分支的匹配守卫为假。接着 Rust 会前往第二个分支,因为这里没有匹配守卫所以会匹配任何 Some 成员。

模式中无法提供类如 if x < 3 的表达能力,我们可以通过匹配守卫的方式来实现。

在之前,我们提到可以使用匹配守卫来解决模式中变量覆盖的问题,那里 match 表达式的模式中新建了一个变量而不是使用 match 之外的同名变量。内部变量覆盖了外部变量,意味着此时不能够使用外部变量的值,下面代码展示了如何使用匹配守卫修复这个问题:

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

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

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

现在这会打印出 Default case, x = Some(1)。现在第二个匹配分支中的模式不会引入一个覆盖外部 y 的新变量 y,这意味着可以在匹配守卫中使用外部的 y。相比指定会覆盖外部 y 的模式 Some(y),这里指定为 Some(n)。此新建的变量 n 并没有覆盖任何值,因为 match 外部没有变量 n

匹配守卫 if n == y 并不是一个模式所以没有引入新变量。这个 y 正是 外部的 y 而不是新的覆盖变量 y,这样就可以通过比较 ny 来表达寻找一个与外部 y 相同的值的概念了。

也可以在匹配守卫中使用 运算符 | 来指定多个模式,同时匹配守卫的条件会作用于所有的模式。下面代码展示了匹配守卫与 | 的优先级。这个例子中看起来好像 if y 只作用于 6,但实际上匹配守卫 if y 作用于 45 6 ,在满足 x 属于 4 | 5 | 6 后才会判断 y 是否为 true

fn main() {
    let x = 1;
    let y = false;
    match x {
        1 | 2 | 3 if y => println!("yes"),
        _ => println!("no"),
    }
}

这个匹配条件表明此分支只匹配 x 值为 123 同时 ytrue 的情况。

虽然在第一个分支中,x 匹配了模式 1 ,但是对于匹配守卫 if y 来说,因为 yfalse,因此该守卫条件的值永远是 false,也意味着第一个分支永远无法被匹配。

@绑定

@运算符允许为一个字段绑定另外一个变量。下面例子中,我们希望测试 Message::Helloid 字段是否位于 3..=7 范围内,同时也希望能将其值绑定到 id_variable 变量中以便此分支中相关的代码可以使用它。我们可以将 id_variable 命名为 id,与字段同名,不过出于示例的目的这里选择了不同的名称。

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

上例会打印出 Found an id in range: 5。通过在 3..=7 之前指定 id_variable @,我们捕获了任何匹配此范围的值并同时将该值绑定到变量 id_variable 上。

第二个分支只在模式中指定了一个范围,id 字段的值可以是 10、11 或 12,不过这个模式的代码并不知情也不能使用 id 字段中的值,因为没有将 id 值保存进一个变量。

最后一个分支指定了一个没有范围的变量,此时确实拥有可以用于分支代码的变量 id,因为这里使用了结构体字段简写语法。不过此分支中没有像头两个分支那样对 id 字段的值进行测试:任何值都会匹配此分支。

当你既想要限定分支范围,又想要使用分支的变量时,就可以用 @ 来绑定到一个新的变量上,实现想要的功能。

@前绑定后解构(Rust 1.56 新增)

使用 @ 还可以在绑定新变量的同时,对目标进行解构:

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    // 绑定新变量 `p`,同时对 `Point` 进行解构
    let p @ Point {x: px, y: py } = Point {x: 10, y: 23};
    println!("x: {}, y: {}", px, py);
    println!("p: {:?}", p);


    let point = Point {x: 10, y: 5};
    if let p @ Point {x: 10, y} = point {
        println!("x is 10 and y is {} in {:?}", y, p);
    } else {
        println!("x was not 10 :(");
    }
}

@新特性(Rust 1.53新增)

fn main() {
    match 1 {
        num @ 1 | 2 => println!("one or two: {}", num),
        _ => println!("anything else"),
    }
}

编译不通过,是因为 num 没有绑定到所有的模式上,只绑定了模式 1,你可能会试图通过这个方式来解决:

num @ (1 | 2)

但是,如果你用的是 Rust 1.53 之前的版本,那这种写法会报错,因为编译器不支持。