枚举和模式匹配

本文大部分内容翻译自:The Rust Programming Language

枚举(enumerations),也被称作enums,允许你通过列举可能的成员(variants)来定义一个类型。

1. 枚举的定义

枚举是除结构体之外,另一种定义自定义数据类型的方式,它与结构体定义自定义类型的方式不同。

例:需要使用IP地址,由于只有IPv4和IPv6两种类型,所以可以枚举这两种可能的类型。任何一个IP地址只可能是IPv4和IPv6中的一个,不可能两者都是,所以枚举很适合处理这种情况,因为一个枚举值也只能是该类型的所有成员中的一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum IpAddrKind {
V4,
V6,
}

fn main() {
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;

route(IpAddrKind::V4);
route(IpAddrKind::V6);
}

fn route(ip_kind: IpAddrKind) {}

现在IpAddrKind就是一个可以在代码中使用的自定义数据类型了。

1.1. 枚举值

创建IpAddrKind两个不同成员的实例:

1
2
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;

foursix,或者IpAddrKind::V4IpAddrKind::V6是同一种类型——IpAddrKind。之后我们就可以定义一个参数为IpAddrKind的函数:

1
fn route(ip_kind: IpAddrKind) {}

任一成员都可以调用route函数:

1
2
route(IpAddrKind::V4);
route(IpAddrKind::V6);

不但定义类型,还定义与类型绑定的值:

1
2
3
4
5
6
7
8
9
10
fn main() {
enum IpAddr {
V4(String),
V6(String),
}

let home = IpAddr::V4(String::from("127.0.0.1"));

let loopback = IpAddr::V6(String::from("::1"));
}

IpAddr enum says that both V4 and V6 variants will have associated String values.

上面定义的每个枚举成员也变成了可以构造枚举实例的函数(function)。

例如:IpAddr::V4()是一个获取String类型的参数、返回IpAddr类型的函数。

不同枚举成员可以有不同类型的数据与它们关联:

1
2
3
4
5
6
7
8
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);

let loopback = IpAddr::V6(String::from("::1"));

还可以为不同枚举成员传入不同的struct参数:

1
2
3
4
5
6
7
8
9
10
11
12
struct Ipv4Addr {
// --snip--
}

struct Ipv6Addr {
// --snip--
}

enum IpAddr {
V4(Ipv4Addr),
V6(Ipv6Addr),
}

可以将任意类型的数据放入枚举成员中,甚至可以存入另一种枚举类型。

1.2. 定义与枚举类型关联的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}

impl Message {
fn call(&self) {
// method body would be defined here
}
}

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

1.3. 枚举类型:Option

Option是标准库定义的一个enum。Option类型应用广泛因为它编码了一个非常普遍的场景,即一个值要么有值要么没值。

Rust中并没有null,不过它有一个可以编码存在不存在概念的枚举。这个枚举是Option<T>

1
2
3
4
enum Option<T> {
None,
Some(T),
}

Option 枚举被包含在了prelude中,因此不需要将它显式引入作用域。它的成员也是如此,不需要用Option::前缀,可以直接使用SomeNone

<T>语法是一个泛型类型参数(generic type parameter)。目前只需要知道:

  • <T>意味着Option枚举的Some成员可以包含任意类型的数据
  • 用在T位置的具体类型的不同,使Option<T>这个整体也变成不同的类型

例:

1
2
3
4
let some_number = Some(5);
let some_string = Some("a string");

let absent_number: Option<i32> = None;

some_number的类型是Option<i32>some_string的类型是Option<&str>,它们是不同的类型。

对于absent_number,我们指定了它的类型:Option<i32>,因为编译器无法推断像推断Some成员那样推断出None是什么类型。

  • 当我们有一个Some值,我们就知道一个值是存在的,并且被保存在Some中;
  • 当我们有一个None值时,在某种意义上,它跟空值(null)具有相同的意义:并没有一个有效的值。

那么,Option<T>为什么就比空值要好呢?

  • 简言之,因为Option<T>T(这里T可以是任何类型)是不同的类型,编译器不允许像使用一个肯定有效的值那样使用 Option。例如:
    1
    2
    3
    4
    let x: i8 = 5;
    let y: Option<i8> = Some(5);

    let sum = x + y;
    会得到编译错误:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    $ cargo run
    Compiling enums v0.1.0 (file:///projects/enums)
    error[E0277]: cannot add `Option<i8>` to `i8`
    --> src/main.rs:5:17
    |
    5 | let sum = x + y;
    | ^ no implementation for `i8 + Option<i8>`
    |
    = help: the trait `Add<Option<i8>>` is not implemented for `i8`

    For more information about this error, try `rustc --explain E0277`.
    error: could not compile `enums` due to previous error
    编译器不允许将Option<i8>i8相加。
  • 当在 Rust 中拥有一个像i8这样类型的值时,它一定是有一个有效的值,我们可以自信地使用它,无需做空值检查。只有当使用 Option<i8>(或者任何用到的类型)的时候需要担心可能没有值。这时,编译器会确保我们在使用这样的值之前处理了为空的情况。
  • 换句话说,在对Option<T>进行T的运算之前,必须将其转换为T。这可以帮助我们捕获到空值最常见的问题之一:假设某值不为空但实际上为空的情况。

想要处理一个可能为空的值,你必须要明确地将它转换成Option<T>类型。这样,当使用这个值时,就需要明确地处理值为空的情况。所以,只要一个值不是Option<T>类型,你都可以安全地认为这个值不是null

这是Rust一个经过深思熟虑的设计决策,来限制空值的泛滥以增加Rust代码的安全性。

为了使用Option<T>类型的值,就需要去处理每个枚举成员(Some、None):

  • 需要处理遇到Some(T)的情况,并允许使用其中的T
  • 需要处理遇到None的情况,这种情况下没有可用的T

match表达式就是处理枚举成员的控制流结构:它会根据枚举成员的不同去运行不同的代码,并且这些代码中可以使用被匹配到的值。

2. match控制流结构

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
  • match关键字后面跟一个表达式(expression)
  • if不同的是,if后面的表达式需要返回Boolean类型,而match可以返回任何类型
  • arms(分支),包括:
    • a pattern
    • some code:是表达式,表达式的结果就是match会返回的结果
      使用=>运算符分隔开。
  • 分支之间用,分隔

2.1. 绑定了值的pattern

1
2
3
4
5
6
7
8
9
10
11
12
13
#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
Alabama,
Alaska,
// --snip--
}

enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}

我们在匹配Coin::Quarter成员的分支的模式中增加了一个叫做state的变量。当匹配到Coin::Quarter时,变量state 将会绑定25美分硬币所对应州的值,接着在那个分支的代码中使用state

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#[derive(Debug)]
enum UsState {
Alabama,
Alaska,
// --snip--
}

enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}

fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {:?}!", state);
25
}
}
}

fn main() {
value_in_cents(Coin::Quarter(UsState::Alaska));
}

2.2. 匹配Option

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}

let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}

2.3. 匹配时穷尽的

下面这段代码有bug,不能编译:

1
2
3
4
5
6
7
8
9
10
11
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}

let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}

我们没有处理None的情况。编译错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ cargo run
Compiling enums v0.1.0 (file:///projects/enums)
error[E0004]: non-exhaustive patterns: `None` not covered
--> src/main.rs:3:15
|
3 | match x {
| ^ pattern `None` not covered
|
= help: ensure that all possible cases are being handled, possibly by adding wildcards or more match arms
= note: the matched value is of type `Option<i32>`

For more information about this error, try `rustc --explain E0004`.
error: could not compile `enums` due to previous error

我们必须穷举到最后一种可能才能使代码有效。

2.4. 通配模式(Catch-all Patterns)和_占位符

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
other => move_player(other),
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}
}

必须将通配分支放在最后,因为模式是按顺序匹配的,如果在通配分支后添加其他分支,Rust会发出警告,因为通配分支后的分支永远不会被匹配到。

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

举例:

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => reroll(), // 明确忽略所有非3和7的值
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn reroll() {}
}

如果在掷出3和7以外的数时,想要什么也不做,可以返回()

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => (),
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}
}

3. if let简洁控制流

3.1. if let

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

只处理Some成员:

1
2
3
4
5
6
7
fn main() {
let config_max = Some(3u8);
match config_max {
Some(max) => println!("The maximum is configured to be {}", max),
_ => (),
}
}

为了满足match表达式“穷尽性”的要求,必须在处理完Some这个唯一的成员后加上_ => (),这样就要增加很多烦人的样板代码。

这种情况下,可以使用更简洁的if let

1
2
3
4
5
6
fn main() {
let config_max = Some(3u8);
if let Some(max) = config_max {
println!("The maximum is configured to be {}", max);
}
}

语法是:

1
2
3
if let pattern = expression {
// code to execute if matched
}

用于匹配的pattern与被匹配的expression之间用=隔开,如果匹配上了,执行花括号内的代码。

3.2. if letelse联用

else代码块的作用跟match_分支一样。

使用match

1
2
3
4
5
6
let coin = Coin::Penny;
let mut count = 0;
match coin {
Coin::Quarter(state) => println!("State quarter from {:?}!", state),
_ => count += 1,
}

使用if letelse

1
2
3
4
5
6
7
let coin = Coin::Penny;
let mut count = 0;
if let Coin::Quarter(state) = coin {
println!("State quarter from {:?}!", state);
} else {
count += 1;
}

参考资料

[1] 枚举和模式匹配:https://kaisery.github.io/trpl-zh-cn/ch06-00-enums.html