枚举(enumerations),也被称作 enums。match 允许我们将一个值与一系列的模式相比较,并根据相匹配的模式执行相应代码。

Rust 程序设计语言学习——枚举&模式匹配-LMLPHP

1 枚举的定义

假设我们要跨省出行,有多种交通工具供选择。常用的交通工具有飞机、火车、汽车和轮船。这是我们常用的跨省出行乘坐交通工具的所有形式:所以可以枚举出所有可能的值,这也正是此枚举名字的由来。

可以通过在代码中定义一个 Vehicle 枚举来表现这个概念并列出可能的交通工具类型,Airplane(飞机)、Train(火车)、Car(汽车) 和 Ship(轮船) 。这被称为枚举的成员(variants):

enum Vehicle {
    Airplane,
    Train,
    Car,
    Ship,
}

如果现在我们要区分汽车到底是哪一种?按照动力类型分类为汽油车、柴油车、混合动力车、电动车和燃料电池车。将动力类型和汽车做关联,修改后的枚举如下:

enum Vehicle {
    Airplane,
    Train,
    Car(PowerType),
    Ship,
}

enum PowerType {
    Gasoline,
    Diesel,
    Mix,
    Electric,
    FuelCell,
}

实际上枚举的成员中可内嵌多种多样的类型,比如下面的例子,一个 Message 枚举,其每个成员都存储了不同数量和类型的值。

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

这个枚举有四个含有不同类型的成员:

  • Quit 没有关联任何数据。
  • Move 类似结构体包含命名字段。
  • Write 包含单独一个 String。
  • ChangeColor 包含三个 i32。

定义一个如上例中所示那样的有关联值的枚举的方式和定义多个不同类型的结构体的方式很相像,除了枚举不使用 struct 关键字以及其所有成员都被组合在一起位于 Message 类型下。如下这些结构体可以包含与之前枚举成员中相同的数据:

struct QuitMessage; // 类单元结构体
struct MoveMessage {
    x: i32,
    y: i32,
}
struct WriteMessage(String); // 元组结构体
struct ChangeColorMessage(i32, i32, i32); // 元组结构体

不过,如果我们使用不同的结构体,由于它们都有不同的类型,我们将不能像使用上例中定义的 Message 枚举那样,轻易的定义一个能够处理这些不同类型的结构体的函数,因为枚举是单独一个类型。

结构体和枚举还有另一个相似点:就像可以使用 impl 来为结构体定义方法那样,也可以在枚举上定义方法。这是一个定义于我们 Message 枚举上的叫做 call 的方法:

    impl Message {
        fn call(&self) {
            // 在这里定义方法体
        }
    }

    let m = Message::Write(String::from("hello"));
    m.call();

方法体使用了 self 来获取调用方法的值。这个例子中,创建了一个值为 Message::Write(String::from("hello")) 的变量 m,而且这就是当 m.call() 运行时 call 方法中的 self 的值。

2 match 控制流结构

match 允许我们将一个值与一系列的模式相比较,并根据相匹配的模式执行相应代码。模式可由字面值、变量、通配符和许多其他内容构成。

可以把 match 表达式想象成某种硬币分类器:硬币滑入有着不同大小孔洞的轨道,每一个硬币都会掉入符合它大小的孔洞。同样地,值也会通过 match 的每一个模式,并且在遇到第一个 “符合” 的模式时,值会进入相关联的代码块并在执行中被使用。

比如我们想得到从一个城市到另一个城市的票价,不同的交通工具价格是不一样的。假设这两座城市无法通过轮渡的方式运输,所以下面的示例中返回了 -1,表征无效。

enum Vehicle {
    Airplane,
    Train,
    Car,
    Ship,
}

fn fare(vehicle: Vehicle) -> i32 {
    match vehicle {
        Vehicle::Airplane => 800,
        Vehicle::Train => 100,
        Vehicle::Car => 200,
        Vehicle::Ship => -1,
    }
}

这看起来非常像 if 所使用的条件表达式,不过这里有一个非常大的区别:对于 if,表达式必须返回一个布尔值,而这里它可以是任何类型的。

match 表达式执行时,它将结果值按顺序与每一个分支的模式相比较。如果模式匹配了这个值,这个模式相关联的代码将被执行。如果模式并不匹配这个值,将继续执行下一个分支,非常类似一个硬币分类器。可以拥有任意多的分支。每个分支相关联的代码是一个表达式,而表达式的结果值将作为整个 match 表达式的返回值。如果分支代码较短的话通常不使用大括号。如果想要在分支中运行多行代码,可以使用大括号,而分支后的逗号是可选的。

enum Vehicle {
    Airplane,
    Train,
    Car,
    Ship,
}

fn fare(vehicle: Vehicle) -> i32 {
    match vehicle {
        Vehicle::Airplane => 800,
        Vehicle::Train => 100,
        Vehicle::Car => 200,
        Vehicle::Ship => {
            println!("Not supported by ships!");
            -1
        }
    }
}

绑定值的模式

匹配分支的另一个有用的功能是可以绑定匹配的模式的部分值。这也就是如何从枚举成员中提取值的。

下面的例子运行后会打印出汽车的动力类型时混动(Mix)。

#[derive(Debug)] 
enum PowerType {
    Gasoline,
    Diesel,
    Mix,
    Electric,
    FuelCell,
}

enum Vehicle {
    Airplane,
    Train,
    Car(PowerType),
    Ship,
}

fn fare(vehicle: Vehicle) -> i32 {
    match vehicle {
        Vehicle::Airplane => 800,
        Vehicle::Train => 100,
        Vehicle::Car(powerType) => {
            println!("The power type of the car is {:?}!", powerType);
            200
        },
        Vehicle::Ship => {
            println!("Not supported by ships!");
            -1
        }
    }
}

fn main() {
    fare(Vehicle::Ship);
    fare(Vehicle::Car(PowerType::Mix));
}

运行结果:

Not supported by ships!
The power type of the car is Mix!

匹配 Option

Rust 并没有空值,不过它确实拥有一个可以编码存在或不存在概念的枚举。这个枚举是 Option<T>,而且它定义于标准库中,如下:

enum Option<T> {
    None,
    Some(T),
}

Option<T> 枚举是如此有用以至于它甚至被包含在了 prelude 之中,你不需要将其显式引入作用域。另外,它的成员也是如此,可以不需要 Option:: 前缀来直接使用 SomeNone。即便如此 Option<T> 也仍是常规的枚举,Some(T)None 仍是 Option<T> 的成员。

<T> 语法是一个我们还未讲到的 Rust 功能。它是一个泛型类型参数。

比如我们想要编写一个函数,它获取一个 Option<i32> ,如果其中含有一个值,将其加一。如果其中没有值,函数应该返回 None 值,而不尝试执行任何操作。

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        Some(i) => Some(i + 1),
    }
}

fn main() {
    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
    println!("six is {:?}, none is {:?}", six, none);
}  

运行结果:

six is Some(6), none is None

如果去除上例中匹配分支内 None => None 这句代码,实际上会报错。

Rust 中的匹配是穷尽的(exhaustive):必须穷举到最后的可能性来使代码有效。

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        Some(i) => Some(i + 1),
    }
}

fn main() {
    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
    println!("six is {:?}, none is {:?}", six, none);
}  

运行结果:

Exited with status 101

Standard Error

   Compiling playground v0.0.1 (/playground)
error[E0004]: non-exhaustive patterns: `None` not covered
   --> src/main.rs:2:11
    |
2   |     match x {
    |           ^ pattern `None` not covered
    |
note: `Option<i32>` defined here
   --> /playground/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/option.rs:570:1
    |
570 | pub enum Option<T> {
    | ^^^^^^^^^^^^^^^^^^
...
574 |     None,
    |     ---- not covered
    = note: the matched value is of type `Option<i32>`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
    |
3   ~         Some(i) => Some(i + 1),
4   ~         None => todo!(),
    |

For more information about this error, try `rustc --explain E0004`.
error: could not compile `playground` (bin "playground") due to 1 previous error

通配模式和 _ 占位符

我们希望对一些特定的值采取特殊操作,而对其他的值采取默认操作。比如下面的例子,只有飞机和火车给出票价,不支持其他交通方式。

#[derive(Debug)]
enum Vehicle {
    Airplane,
    Train,
    Car,
    Ship,
}

fn fare(vehicle: Vehicle) -> i32 {
    match vehicle {
        Vehicle::Airplane => 800,
        Vehicle::Train => 100,
        other => {
            println!("Not support {:?}", other);
            -1
        }
    }
}

fn main() {
    let price = fare(Vehicle::Airplane);
    println!("The price of air tickets is {}", price);
    fare(Vehicle::Car);
}

运行结果:

The price of air tickets is 800
Not support Car

对于前两个分支,匹配模式是字面值 Vehicle::AirplaneVehicle::Train,最后一个分支则涵盖了所有其他可能的值,模式是我们命名为 other 的一个变量。

即使我们没有列出 Vehicle 所有可能的值,这段代码依然能够编译,因为最后一个模式将匹配所有未被特殊列出的值。这种通配模式满足了 match 必须被穷尽的要求。请注意,我们必须将通配分支放在最后,因为模式是按顺序匹配的。如果我们在通配分支后添加其他分支,Rust 将会警告我们,因为此后的分支永远不会被匹配到。

Rust 还提供了一个模式,当我们不想使用通配模式获取的值时,请使用 _ ,这是一个特殊的模式,可以匹配任意值而不绑定到该值。这告诉 Rust 我们不会使用这个值,所以 Rust 也不会警告我们存在未使用的变量。

#[derive(Debug)]
enum Vehicle {
    Airplane,
    Train,
    Car,
    Ship,
}

fn fare(vehicle: Vehicle) -> i32 {
    match vehicle {
        Vehicle::Airplane => 800,
        Vehicle::Train => 100,
        _ => {
            println!("Not support");
            -1
        }
    }
}

fn main() {
    let price = fare(Vehicle::Airplane);
    println!("The price of air tickets is {}", price);
    fare(Vehicle::Car);
}

运行结果:

The price of air tickets is 800
Not support

这个例子也满足穷举性要求,因为我们在最后一个分支中明确地忽略了其他的值。我们没有忘记处理任何东西。

3 if let 简洁控制流

if let 语法让我们以一种不那么冗长的方式结合 iflet,来处理只匹配一个模式的值而忽略其他模式的情况。

enum Vehicle {
    Airplane,
    Train,
    Car,
    Ship,
}

fn fare(vehicle: Vehicle) -> i32 {
    if let Vehicle::Airplane = vehicle {
        println!("Travel by plane");
        return 800;
    }
    return -1;
}

fn main() {
    let mut price = fare(Vehicle::Airplane);
    println!("The price of air tickets is {}", price);
    price = fare(Vehicle::Car);
    println!("The price of car tickets is {}", price);
}

运行结果:

Travel by plane
The price of air tickets is 800
The price of car tickets is -1

if let 语法获取通过等号分隔的一个模式和一个表达式。它的工作方式与 match 相同,这里的表达式对应 match 而模式则对应第一个分支。在这个例子中,模式是 Vehicle::Airplane。就跟在对应的 match 分支中一样。模式不匹配时 if let 块中的代码不会执行。

使用 if let 意味着编写更少代码,更少的缩进和更少的样板代码。然而,这样会失去 match 强制要求的穷尽性检查。matchif let 之间的选择依赖特定的环境以及增加简洁度和失去穷尽性检查的权衡取舍。

换句话说,可以认为 if letmatch 的一个语法糖,它当值匹配某一模式时执行代码而忽略所有其他值。

还可以在 if let 中包含一个 else

enum Vehicle {
    Airplane,
    Train,
    Car,
    Ship,
}

fn fare(vehicle: Vehicle) -> i32 {
    if let Vehicle::Airplane = vehicle {
        println!("Travel by plane");
        return 800;
    } else {
        return -1;
    }
}

fn main() {
    let mut price = fare(Vehicle::Airplane);
    println!("The price of air tickets is {}", price);
    price = fare(Vehicle::Train);
    println!("The price of train tickets is {}", price);
}

运行结果:

Travel by plane
The price of air tickets is 800
The price of train tickets is -1

参考链接

  1. Rust 官方网站:https://www.rust-lang.org/zh-CN
  2. Rust 官方文档:https://doc.rust-lang.org/
  3. Rust Play:https://play.rust-lang.org/
  4. 《Rust 程序设计语言》
04-06 10:48