Skip to content

7.2 match

首先,我们看看使用 match 的最简单的示例:

rust
enum Direction {
    East, West, South, North
}

fn print(x: Direction)
{
    match x {
        Direction::East  => {
            println!("East");
        }
        Direction::West  => {
            println!("West");
        }
        Direction::South => {
            println!("South");
        }
        Direction::North => {
            println!("North");
        }
    }
}

fn main() {
    let x = Direction::East;
    print(x);
}

当一个类型有多种取值可能性的时候,特别适合使用 match 表达式。 对于这个示例,我们也可以用多个 if-else 表达式完成类似的功能,但是 match 表达式还有更强大的功能,下面我们继续说明。

7.2.1 exhaustive

如果我们把上例中的Direction::North对应的分支删除:


rust
match x {
    Direction::East  => {
        println!("East");
    }
    Direction::West  => {
        println!("West");
    }
    Direction::South => {
        println!("South");
    }
}

编译,出现了编译错误:

txt
error[E0004]: non-exhaustive patterns: `North` not covered

这是因为 Rust 要求 match 需要对所有情况做完整的、无遗漏的匹配,如果漏掉了某些情况,是不能编译通过的。exhaustive 意思是无遗漏的、穷尽的、彻底的、全面的。exhaustive 是 Rust 模式匹配的重要特点。

有些时候我们不想把每种情况一一列出,可以用一个下划线来表达“除了列出来的那些之外的其他情况”:

rust
match x {
    Direction::East  => {
        println!("East");
    }
    Direction::West  => {
        println!("West");
    }
    Direction::South => {
        println!("South");
    }
    _ => {
        println!("Other");
    }
}

正因为如此,在多个项目之间有依赖关系的时候,在上游的一个库中对 enum 增加成员,是一个破坏兼容性的改动。因为增加成员后,很可能会导致下游的使用者 match 语句编译不过。为解决这个问题,Rust 提供了一个叫作non_exhaustive的功能(目前还没有稳定)。示例如下:


rust
#[non_exhaustive]
pub enum Error {
    NotFound,
    PermissionDenied,
    ConnectionRefused,
}

上游库作者可以用一个叫作“non_exhaustive”的 attribute 来标记一个 enum 或者 struct,这样在另外一个项目中使用这个类型的时候,无论如何都没办法在 match 表达式中通过列举所有的成员实现完整匹配,必须使用下划线才能完成编译。这样,以后上游库里面为这个类型添加新成员的时候,就不会导致下游项目中的编译错误了因为它已经存在一个默认分支匹配其他情况。

7.2.2 下划线

下划线还能用在模式匹配的各种地方,用来表示一个占位符,虽然匹配到了但是忽略它的值的情况:


rust
struct P(f32, f32, f32);

fn calc(arg: P) -> f32 {
    // 匹配 tuple struct,但是忽略第二个成员的值
    let P(x, _, y) = arg;
    x * x + y * y
}

fn main() {
    let t = P(1.0, 2.0, 3.0);
    println!("{}", calc(t));
}

对于上例,实际上我们还能写得更简略一点。因为函数参数本身就具备“模式解构”功能,我们可以直接在参数中完成解构:


rust
struct P(f32, f32, f32);
// 参数类型是 P,参数本身是一个模式,解构之后,变量 x、y 分别绑定了第一个和第三个成员
fn calc(P(x, _, y): P) -> f32 {
    x * x + y * y
}
fn main() {
    let t = P(1.0, 2.0, 3.0);
    println!("{}", calc(t));
}

另外需要提醒的一点是,下划线更像是一个“关键字”,而不是普通的“标识符”(identifier),把它当成普通标识符使用是会有问题的。举例如下:


rust
fn main() {
    let _ = 1_i32;
    let x = _ + _;
    println!("{}", x);
}

编译可见,编译器并不会把单独的下划线当成一个正常的变量名处理:


rust
error: expected expression, found `_`
 --> test.rs:4:13
  |
4 |     let x = _ + _;
  |             ^

error[E0425]: cannot find value `x` in this scope

如果把下划线后面跟上字母、数字或者下划线,那么它就可以成为一个正常的标识符了。比如,连续两个下划线__,就是一个合法的、正常的“标识符”。

let _ = x;let _y = x;具有不一样的意义。这一点在后面的“析构函数”部分还会继续强调。如果变量x是非 Copy 类型,let _ = x;的意思是“忽略绑定”,此时会直接调用x的析构函数,我们不能在后面使用下划线_读取这个变量的内容;而let _y = x;的意思是“所有权转移”,_y是一个正常的变量名,x的所有权转移到了_y上,_y在后面可以继续使用。

下划线在 Rust 里面用处很多,比如:在 match 表达式中表示“其他分支”,在模式中作为占位符,还可以在类型中做占位符,在整数和小数字面量中做连接符,等等。

除了下划线可以在模式中作为“占位符”,还有两个点..也可以在模式中作为“占位符”使用。下划线表示省略一个元素,两个点可以表示省略多个元素。比如:

rust
fn main() {
    let x = (1, 2, 3);
    let (a, _) = x;    // 模式解构
    println!("{}", a);
}

如果我们希望只匹配 tuple 中的第一个元素,其他的省略,那么用一个下划线是不行的,因为这样写,左边的 tuple 和右边的 tuple 不匹配。修改方案有两种。一种是:


rust
let (a, _, _) = x; // 用下划线,那么个数要匹配

另一种是:


rust
let (a, ..) = x;   // 用两个点,表示其他的全部省略
let (a, .., b) = x;// 用两个点,表示只省略所有元素也是可以的

7.2.3 match 也是表达式

跟 Rust 中其他流程控制语法一样,match 语法结构也同样可以是表达式的一部分。示例如下:


rust
enum Direction {
    East, West, South, North
}

fn direction_to_int(x: Direction) -> i32
{
    match x {
        Direction::East  => 10,
        Direction::West  => 20,
        Direction::South => 30,
        Direction::North => 40,
    }
}

fn main() {
    let x = Direction::East;
    let s = direction_to_int(x);
    println!("{}", s);
}

match 表达式的每个分支可以是表达式,它们要么用大括号包起来,要么用逗号分开。每个分支都必须具备同样的类型。在此例中,这个 match 表达式的类型为 i32,在 match 后面没有分号,因此这个表达式的值将会作为整个函数的返回值传递出去。

match 除了匹配“结构”,还可以匹配“值”:


rust
fn category(x: i32) {
    match x {
        -1 => println!("negative"),
        0  => println!("zero"),
        1  => println!("positive"),
        _  => println!("error"),
    }
}
fn main() {
    let x = 1;
    category(x);
}

我们可以使用或运算符|来匹配多个条件,比如:


rust
fn category(x: i32) {
    match x {
        -1 | 1 => println!("true"),
        0  => println!("false"),
        _  => println!("error"),
    }
}

fn main() {
    let x = 1;
    category(x);
}

我们还可以使用范围作为匹配条件,使用..表示一个前闭后开区间范围,使用..=表示一个闭区间范围:


rust
let x = 'X';

match x {
    'a' ..= 'z' => println!("lowercase"),
    'A' ..= 'Z' => println!("uppercase"),
    _ => println!("something else"),
}

7.2.4 Guards

可以使用 if 作为“匹配看守”(match guards)。当匹配成功且符合 if 条件,才执行后面的语句。示例如下:


rust
enum OptionalInt {
    Value(i32),
    Missing,
}

let x = OptionalInt::Value(5);

match x {
    OptionalInt::Value(i) if i > 5 => println!("Got an int bigger than five!"),
    OptionalInt::Value(..) => println!("Got an int!"),
    OptionalInt::Missing => println!("No such luck."),
}

在对变量的“值”进行匹配的时候,编译器依然会保证“完整无遗漏”检查。但是这个检查目前做得并不是很完美,某些情况下会发生误报的情况,因为毕竟编译器内部并没有一个完整的数学解算功能:


rust
fn main() {
    let x = 10;

    match x {
        i if i > 5 => println!("bigger than five"),
        i if i <= 5 => println!("small or equal to five"),
    }
}

从 if 条件中可以看到,实际上我们已经覆盖了所有情况,可惜还是出现了编译错误。编译器目前还无法完美地处理这样的情况。我们只能再加入一条分支,单纯为了避免编译错误:

rust
_ => unreachable!(),

编译器会保证 match 的所有分支合起来一定覆盖了目标的所有可能取值范围,但是并不会保证各个分支是否会有重叠的情况(毕竟编译器不想做成一个完整的数学解算器)。 如果不同分支覆盖范围出现了重叠,各个分支之间的先后顺序就有影响了:

rust
fn intersect(arg: i32) {
    match arg {
        i if i < 0 => println!("case 1"),
        i if i < 10 => println!("case 2"),
        i if i * i < 1000 => println!("case 3"),
        _ => println!("default case"),
    }
}

fn main() {
    let x = -1;
    intersect(x);
}

如果我们进行匹配的值同时符合好几条分支,那么总会执行第一条匹配成功的分支,忽略其他分支。

7.2.5 变量绑定

我们可以使用@符号绑定变量。@符号前面是新声明的变量,后面是需要匹配的模式:

rust
let x = 1;

match x {
    e @ 1 ..= 5 => println!("got a range element {}", e),
    _ => println!("anything"),
}

当一个 Pattern 嵌套层次比较多的时候,如果我们需要匹配更深层次作为条件,希望绑定上面一层的数据,就需要像下面这样写:

rust
#![feature(exclusive_range_pattern)]

fn deep_match(v: Option<Option<i32>>) -> Option<i32> {
    match v {
        // r 绑定到的是第一层 Option 内部,r 的类型是 Option<i32>
        // 与这种写法含义不一样:Some(Some(r)) if (1..10).contains(r)
        Some(r @ Some(1..10)) => r,
        _ => None,
    }
}

fn main() {
    let x = Some(Some(5));
    println!("{:?}", deep_match(x));

    let y = Some(Some(100));
    println!("{:?}", deep_match(y));
}

如果在使用@的同时使用|,需要保证在每个条件上都绑定这个名字:

rust
let x = 5;

match x {
    e @ 1 .. 5 | e @ 8 .. 10 => println!("got a range element {}", e),
    _ => println!("anything"),
}

7.2.6 ref 和 mut

如果我们需要绑定的是被匹配对象的引用,则可以使用ref关键字:

rust
let x = 5_i32;

match x {
    ref r => println!("Got a reference to {}", r), // 此时 r 的类型是 `&i32`
}

之所以在某些时候需要使用 ref,是因为模式匹配的时候有可能发生变量的所有权转移,使用 ref 就是为了避免出现所有权转移。

那么 ref 关键字和引用符号&有什么关系呢?考虑以下代码中变量绑定x分别是什么类型?


rust
let x = 5_i32;      // i32
let x = &5_i32;     // &i32
let ref x = 5_i32;  // ???
let ref x = &5_i32; // ???

注意:ref 是“模式”的一部分,它只能出现在赋值号左边,而&符号是借用运算符,是表达式的一部分,它只能出现在赋值号右边。

为了搞清楚这些变量绑定的分别是什么类型,我们可以把变量的类型信息打印出来看看。有两种方案:

  • 利用编译器的错误信息来帮我们理解;

  • 利用标准库里面的intrinsic函数打印。

方案一,示例如下:


rust
// 这个函数接受一个 unit 类型作为参数
fn type_id(_: ()) {}

fn main() {
    let ref x = 5_i32;
    // 实际参数的类型肯定不是 unit,此处必定有编译错误,通过编译错误,我们可以看到实参的具体类型
    type_id(x);
}

这里我们写了一个不做任何事情的函数type_id。它接收一个参数,类型是(),我们在main函数中调用这个函数,肯定会出现类型不匹配的编译错误。错误信息为:


rust
error[E0308]: mismatched types
 --> test.rs:5:13
  |
5 |     type_id(x);
  |             ^ expected (), found &i32
  |
  = note: expected type `()`
             found type `&i32`

这个错误信息正是我们想要的内容。从中我们可以看到我们感兴趣的变量类型。

方案二,示例如下:


rust
#![feature(core_intrinsics)]

fn print_type_name<T>(_arg: &T) {
    unsafe {
        println!("{}", std::intrinsics::type_name::<T>());
    }
}

fn main() {
    let ref x = 5_i32;
    print_type_name(&x);
}

利用标准库里面提供的type_name函数,可以打印出变量的类型信息。

从以上方案可以看到,let ref x = 5_i32;let x = &5_i32;这两条语句中,变量x是同样的类型&i32

同理,我们可以试验得出,let ref x = &5_i32;语句中,变量 x 绑定的类型是&&i32。对于更复杂的情况,读者可以用类似的办法做试验,多看看各种情况下具体的类型是什么。

ref 关键字是“模式”的一部分,不能修饰赋值号右边的值。let x = ref 5_i32;这样的写法是错误的语法。

mut 关键字也可以用于模式绑定中。mut 关键字和 ref 关键字一样,是“模式”的一部分。Rust 中,所有的变量绑定默认都是“不可更改”的。只有使用了 mut 修饰的变量绑定才有修改数据的能力。最简单的例子如下:


rust
fn main() {
    let x = 1;
    x = 2;
}

编译错误,错误信息为:error:re-assignment of immutable variable x。我们必须使用let mut x=1;,才能在以后的代码中修改变量绑定x

使用了 mut 修饰的变量绑定,可以重新绑定到其他同类型的变量。


rust
fn main() {
    let mut v = vec![1i32, 2, 3];
    v = vec![4i32, 5, 6];         // 重新绑定到新的 Vec
    v = vec![1.0f32, 2, 3];       // 类型不匹配,不能重新绑定
}

重新绑定与前面提到的“变量遮蔽”(shadowing)是完全不同的作用机制。“重新绑定”要求变量本身有mut修饰,并且不能改变这个变量的类型。“变量遮蔽”要求必须重新声明一个新的变量,这个新变量与老变量之间的类型可以毫无关系。

Rust 在“可变性”方面,默认为不可修改。与 C++ 的设计刚好相反。C++ 默认为可修改,使用const关键字修饰的才变成不可修改。

mut关键字不仅可以在模式用于修饰变量绑定,还能修饰指针(引用),这里是很多初学者常常搞混的地方。mut修饰变量绑定,与&mut型引用,是完全不同的意义。


rust
let mut x: &mut i32;
//  ^1      ^2

以上两处的 mut 含义是不同的。第 1 处 mut,代表这个变量 x 本身可变,因此它能够重新绑定到另外一个变量上去,具体到这个示例来说,就是指针的指向可以变化。第 2 处 mut,修饰的是指针,代表这个指针对于所指向的内存具有修改能力,因此我们可以用*x = 1;这样的语句,改变它所指向的内存的值。

mut 关键字不像想象中那么简单,我们会经常碰到与 mut 有关的各种编译错误。在本节中,只是简单介绍了它的语法,关于 mut 关键字代表的深层含义,及其正确使用方法,在本书第二部分还会有详细描述。

至于为什么有些场景下,我们必须使用 ref 来进行变量绑定,背后的原因跟“move”语义有关。关于变量的生命周期、所有权、借用、move 等概念,请参考本书第二部分。下面举个例子:


rust
fn main() {
    let mut x : Option<String> = Some("hello".into());
    match x {
        Some(i) => i.push_str("world"),
        None => println!("None"),
    }

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

这段代码编译器会提示编译错误,第一个原因是,局部变量i是不可变的,所以它无权调用push_str方法。我们可以修改为Some(mut i)再次编译。还是会发生编译错误。这次提示的是,“use of partially moved value 'x'”。因为编译器认为这个 match 语句把内部的 String 变量移动出来了,所以后续的打印x的值是错误的行为。为了保证这个 match 语句不发生移动,我们需要把这个模式继续修改为Some(ref mut i),这一次,编译通过了。

这个问题还有更简单的修复方式,就是把match x改为match &mut x


rust
fn main() {
    let mut x : Option<String> = Some("hello".into());
    match &mut x {
        Some(i) => i.push_str("world"),
        None => println!("None"),
    }

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

在这种情况下,编译器没有报错,是因为我们对指针做了模式匹配,编译器很聪明地推理出来了,变量i必须是一个指针类型,所以它帮我们自动加了 ref mut 模式,它通过自动类型推导自己得出了结论,认为i的类型是&mut String。这是编译器专门做的一个辅助功能。

在很多时候,特别是类型嵌套层次很多的时候,处处都要关心哪个 pattern 是不是要加个 mut 或者 ref,其实是个很烦人的事情。有了这个功能,用户就不用每次都写麻烦的 mut 或者 ref,在一些显而易见的情况下,编译器自动来帮我们合理地使用 mut 或者 ref。 读者可以用我们前面实现的print_type_name试试,用Some(i)模式,以及用Some(ref mut i)模式,变量i的类型分别是什么。结论是类型一样。 因为在我们不明确写出来 ref mut 模式的时候,编译器帮我们做了更合理的自动类型推导。

Released under the MIT License