0%

Rust 权威指南(05):枚举与模式匹配

枚举类型也称为 枚举,允许我们列举所有可能的值来定义一个类型。在 Rust 的 match 表达式中使用模式匹配,可以根据不同的枚举值来执行不同的代码。

定义枚举

如下通过定义枚举 IpAddrKind 来列举出所有可能的 IP 地址种类,即 枚举变体枚举的变体全部位于其标识符的命名空间中,并使用两个冒号来将标识符和变体分隔开来

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

fn route(ip_type: IpAddrKind) {

}

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

route(four);
route(six);
}

为了存储实际的 IP 地址数据,如果使用类似于 C 的编程语言,我们可能会写出如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
enum IpAddrKind {
V4,
V6,
}

struct IpAddr {
kind: IpAddrKind,
address: String,
}

fn main() {
let four = IpAddr {
kind: IpAddrKind::V4,
address: String::from("127.0.0.1"),
};

let six = IpAddr {
kind: IpAddrKind::V6,
address: String::from("::1"),
};
}

但是 Rust 的枚举允许我们直接将关联的数据嵌入枚举变体内,可以使用枚举来更简捷地表达上述概念,而不用将枚举集成到结构体中。如下 IpAddr 的定义中,V4 和 V6 两个变体都关联上了 String 值:

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

fn main() {
let four = IpAddr::V4(String::from("127.0.0.1"));
let six = IpAddr::V4(String::from("::1"));
}

直接将数据附加到枚举的每个变体中,这样就不需要额外地使用结构体了。另外一个使用枚举替代结构体的优势在于:每个变体可以拥有不同类型和数量的关联数据。可以在枚举的变体中嵌入任意类型的数据,无论是字符串、数值还是结构体,甚至可以嵌入另外一个枚举。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct Ipv4Addr {

}

struct Ipv6Addr {

}

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

fn main() {
let four = IpAddr::V4(Ipv4Addr{});
let six = IpAddr::V6(Ipv6Addr{});
}

当我们使用结构体时,每个结构体都有自己的类型,我们无法轻易地定义一个能够统一处理这些类型数据的函数。而对于枚举的变体则没有这个问题,因为它们都是同一个类型。

和结构体类似,也是使用 impl 关键字定义枚举的方法

Option 枚举及其在空值处理方面的优势

Option 类型定义于标准库中,它描述了一种值可能不存在的情形,所以它被广泛地应用于各种场合。将这一概念使用类型系统描述出来意味着,编译器可以自动检查我们是否妥善地处理了所有应该被处理的情况。使用这一功能可以避免在某些语言中极其常见的错误。

空值本身所尝试表达的概念仍然是有意义的,因为它代表了因为某种原因而变为无效或者缺失的值。引发问题的关键不是概念本身,而是那些具体的实现措施。Rust 中没有空值,但是提供了一个使用类似概念的枚举,可以用它来标识一个值无效或者缺失,该枚举就是 Option<T>,它在标准库中的定义如下:

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

Option<T> 及其变体都包含在预导入模块中。

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

let absent_number: Option<i32> = None;
}

当使用 None 而不是 Some 变体来赋值时,需要明确地告诉 Rust 该 Option<T> 的具体类型。这是因为单独的 None 变体值与持有数据的 Some 变体不一样,编译器无法根据这些信息来正确推导出值的完整类型。

Option<T> 和 T 是不同的类型,编译器不允许我们直接像使用普通值那样去使用 Option<T> 的值。当我们使用 Option<T> 时,我们必须考虑值不存在的情况,同时编译器会迫使我们在使用值之前正确地做出处理动作。为了使用 Option<T> 中可能存在的 T,必须将它转换为 T,这样能帮助我们避免使用空值时最常见的一个问题:假设某个值存在,实际上却为空。

为了持有一个可能为空的值,我们总是需要将它显式地放入对应类型的 Option 值中。当我们随后使用该值的时候,也必须显式地处理它可能为空的情况。而在其他地方,只有值的类型不是 Option<T>,我们就可以安全地假设这个值不是非空的。这是 Rust 为限制控制泛滥以增加 Rust 代码安全性而做出的一个有意为之的设计决策。

为了使用一个 Option<T> 值,必须要编写处理每个变体的代码。match 表达式就是这么一个可以用来处理枚举的控制流结构:它允许我们基于枚举拥有的变体来决定运行的代码分支,并允许代码通过匹配值来获取变体内的数据。

控制流运算符 match

Rust 的控制流运算符 match 允许将一个值与一系列的模式相比较,并根据匹配的模式执行相应代码。模式可以由字面量、变量名、通配符等组成。值会依次通过 match 中的模式,并在遇到第一个 符合 的模式时进入相关联的代码块,并在执行过程中被代码所使用。

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
enum Coin {
Penny,
Nickle,
Dime,
Quarter,
}


fn value_in_cents(coin: Coin) -> u32 {
match coin {
Coin::Penny => {
println!("lucky penny");
1
},
Coin::Nickle => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}

fn main() {
println!("{}", value_in_cents(Coin::Penny));
println!("{}", value_in_cents(Coin::Nickle));
println!("{}", value_in_cents(Coin::Dime));
println!("{}", value_in_cents(Coin::Quarter));
}
  • match 中的表达式可以返回任何类型
  • match 中的分支由模式和它所关联的代码完成,模式与分支代码使用 => 运算符区分开来
  • 分支关联的代码也是一个表达式,它运行得到的结果值会作为整个 match 表达式的结果返回
  • 如果一个分支所关联的代码包含多行,可以使用 {} 将它们包裹起来
  • 不同分支之间使用逗号分隔

当 match 表达式执行时,它会将产生的结果依次与每个分支中的模式进行比较,如果匹配成功,则与该模式相关联的代码就会继续执行,而模式匹配失败,则会继续执行下一个分支。

匹配分支可以绑定被匹配对象的部分值,这也是我们用于从枚举变体中提取值的方法

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
30
31
32
33
34
35
#[derive(Debug)]
enum State {
Alabama,
Alaska,
}

enum Coin {
Penny,
Nickle,
Dime,
Quarter(State),
}


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

fn main() {
println!("{}", value_in_cents(Coin::Penny));
println!("{}", value_in_cents(Coin::Nickle));
println!("{}", value_in_cents(Coin::Dime));
println!("{}", value_in_cents(Coin::Quarter(State::Alaska)));
}

匹配 Option<T>

在使用 Option<T> 时,也可以使用 match 来从 Some 中取得其内部的值。

1
2
3
4
5
6
7
8
9
10
11
12
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);
}

Rust 代码中将 match 与枚举相结合在许多情形下都是非常有用的:使用 match 来匹配枚举值,并将其中的值绑定到某个变量上,接着根据这个值执行相应的代码

匹配必须穷举所有可能

Rust 中的匹配是穷尽的:必须穷尽所有可能来确保代码是合法有效的。有时候可能不想处理所有的可能值,_ 模式可以匹配任何值,在它对应的代码块中,可以使用一个空元祖 (),这样什么都不会发生。

简单控制流 if let

if let 可以让我们通过一种不那么繁琐的语法结合使用 if 与 let,并处理那些只用关心某一种匹配而忽略其他匹配的情况。例如如下代码指向处理 Some(3),而其他情况都直接忽略。

1
2
3
4
5
let some_u8_value = Some(0u8);
match some_u8_value {
Some(3) => println!("three),
_ => (),
}

if let 可以让上述代码更加简单:

1
2
3
if let Some(3) = some_u8_value {
println!("three");
}

if let 语法使用一对以 = 隔开的模式与表达式,它们所起的作用与 match 中完全相同,表达式对应 match 中的输入,而模式则对应第一个分支。使用 if let 更加简洁,但是也放弃了 match 所附带的穷尽性检查。

if let 搭配中还可以使用 else,else 所关联的代码块在 if let 语句中扮演的角色,类似于 match 中的 _ 模式所关联的代码块一样。

1
2
3
4
5
if let Some(3) = some_u8_value {
println!("three");
} else {
println!("not three");
}