枚举类型(enum),通常也被简称为枚举,它允许我们列举所有可能的值来定义一个类型。
枚举搭配 match
表达式使用模式匹配,可以根据不同的枚举值来执行不同的代码。
Rust 中的枚举更类似于 Haskell 这类函数式编程语言中的代数数据类型(ADT)。
定义枚举
假设我们需要对 IP 地址进行处理。目前只有两种广泛被使用的 IP 地址标准:IPv4 和 IPv6。
我们只需要处理这两种情形,且一个地址要么是 IPv4,要么是 IPv6,因此可以使用枚举将所有可能的值(IPv4 和 IPv6)列举出来,作为一种新的数据类型。
1 | enum IpAddrKind { |
现在,IpAddrKind
就是一个可以在代码中随处使用的自定义数据类型了。
枚举值
可以参照下面的代码使用 IpAddrKind
中的两个变体(V4
和 V6
)创建实例:1
2let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
由于 IpAddrKind:V4
和 IpAddrKind:V6
拥有相同的类型(都是 IpAddrKind
),我们可以定义一个接收 IpAddrKind
类型参数的函数来统一处理它们:1
fn route(ip_type: IpAddrKind) { }
现在,我们可以使用任意一个变体来调用这个函数了:1
2route(IpAddrKind::V4);
route(IpAddrKind::V6);
当前定义的枚举类型 IpAddrKind
,还只能区分 IP 地址的种类,没有办法去存储实际的 IP 地址数据。
可以使用结构体来解决这个问题:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19enum IpAddrKind {
V4,
V6,
}
struct IpAddr {
kind: IpAddrKind,
address: String,
}
let home = IpAddr {
kind: IpAddrKind::V4,
address: String::from("127.0.0.1"),
};
let loopback = IpAddr {
kind: IpAddrKind::V6,
address: String::from("::1"),
};
实际上,我们可以直接将枚举关联的数据嵌入其变体内,而不用像上面那样将枚举集成至结构体中。
下面的代码直接定义了 IpAddr
枚举,V4
和 V6
两个变体都被关联上了一个 String 值:1
2
3
4
5
6
7
8enum IpAddr {
V4(String),
V6(String),
}
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));
我们直接将数据附加到枚举的每个变体中,就不需要额外地使用结构体了。
另外一个枚举替代结构体的优势在于,每个变体可以拥有不同类型和数量的关联数据,同时所有变体仍属于同一个枚举类型。1
2
3
4
5
6
7
8enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
参考下面代码中定义的一个 Message
枚举:1
2
3
4
5
6enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
该枚举拥有 4 个内嵌了不同类型数据的变体:
- Quit 没有关联任何数据
- Move 包含了一个匿名结构体
- Write 包含了一个 String
- ChangeColor 包含了 3 个 i32 值
枚举有些类似于定义多个不同类型的结构体。但枚举除了不会使用 struct
关键字,还将变体们组合到了同一个 Message
类型中。
下面代码中的结构体可以存储与这些变体完全一样的数据:1
2
3
4
5
6
7struct QuitMessage; // 空结构体
struct MoveMessage {
x: i32,
y: i32,
}
struct WriteMessage(String); // 元组结构体
struct ChangeColorMessage(i32, i32, i32); // 元组结构体
两种实现方式的差别在于,如果使用了不同的结构体,则每个结构体都会拥有自己的类型,无法轻易定义一个统一处理这些类型的函数。而前面的 Message
枚举是单独的一个类型。
正如我们可以用 impl
关键字定义结构体的方法一样,我们同样可以为 Message
定义自己的方法:1
2
3
4
5
6
7
8
9
10impl Message {
fn call(&self) {
// 方法在这里定义
}
}
fn main() {
let m = Message::Write(String::from("hello"));
m.call();
}
Option 枚举及空值处理
Option
是一种定义于标准库中的枚举类型,它描述了一种值可能不存在的情形。借助类型系统,编译器可以自动检查我们是否妥善地处理了所有应该被处理的情况。
Rust 没有像其他语言一样支持空值(Null)。空值本身是一个值,但它的含义却是没有值。
空值的问题在于,当你尝试像使用非空值那样使用空值时,就会触发某种程度上的错误。由于空或非空的属性广泛散布在程序中,因此很难避免引起此类问题。
但空值本身所尝试表达的概念仍是有意义的,它代表了因为某种原因而变得无效或缺失的值。
Rust 中虽然没有空值,但提供了一个拥有类似概念的枚举 Option<T>
,它可以用来标识一个值无效或缺失。Option<T>
在标准库中的定义如下:1
2
3
4enum Option<T> {
Some(T),
None,
}
Option<T>
是一个普通的枚举类型,Some<T>
和 None
是该类型的变体。1
2
3
4let some_number = Some(5);
let some_string = Some("a string");
let absent_number: Option<i32> = None;
若使用 None
而不是 Some
变体来进行赋值,则需要明确声明这个 Option<T>
的具体类型,否则编译器无法进行类型推导。
当我们有了一个 Some
值时,就可以确定值是存在的,并且被 Some
所持有;当我们有了一个 None
值时,就知道当前并不存在一个有效的值。Option<T>
的设计相对于空值的优势在于,Option<T>
和 T
是不同的类型,编译器不会允许我们像使用普通值一样直接去使用 Option<T>
的值。如:1
2
3
4let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y;
运行上述代码会导致编译器报错,因为 i8
和 Option<i8>
是不同的类型。
当我们持有的类型是 i8
时,编译器可以确保该值是有效的。但是当我们持有的类型是 Option<i8>
时,我们必须要考虑值不存在的情况,编译器会迫使我们在使用值之前正确地做出处理操作。
为了持有一个可能为空的值,我们总是需要将其显式地放入对应类型的 Option<T>
值当中。当我们随后使用这个值时,也必须显式地处理它可能为空的情况。
即在处理 Option<T>
时,必须编写应对每个变体的代码。某些代码只会在持有 Some(T)
值时运行,它们可以使用变体中存储的 T
;另外一些代码则只会在持有 None
值时运行,这些代码没有可用的 T
值。
match
表达式就是一种可以用来处理 Option<T>
这类枚举的控制流结构。它允许我们基于枚举拥有的变体来决定运行的代码分支,并允许代码通过模式匹配来获取变体内的数据。
控制流运算符 match
match
是 Rust 中一个强大的控制流运算符,它允许将一个值与一系列模式相比较,并根据匹配的模式执行相应的代码。这些模式可以由字面量、变量名、通配符及许多其他东西组成。
下面的代码会接收一个美国的硬币作为输入,确定硬币的类型并返回其分值:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u32 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
fn main() {
let coin = Coin::Dime;
println!("{}", value_in_cents(coin));
}
每个 match
分支所关联的代码同时也是一个表达式,这个表达式运行的结果同时也会作为整个 match
表达式的结果返回。
绑定值的模式
匹配分支还可以绑定匹配对象的部分值,这使得我们能够从枚举变体中提取特定的值。
比如美国的 25 美分硬币 50 个州采用了不同的设计。现在将这些信息添加至枚举中: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// 方便打印输出默认不支持打印的类型
enum UsState {
Alabama,
Alaska,
// ...
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn value_in_cents(coin: Coin) -> u32 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {:?}.", state);
25
}
}
}
fn main() {
let alaska = UsState::Alaska;
let coin = Coin::Quarter(alaska);
value_in_cents(coin);
// => State quarter from Alaska.
}
上面的代码中,我们在模式中加入了一个名为 state
的变量用于匹配变体 Coin::Quarter
中的值。当匹配到 Coin::Quarter
时,变量 state
就会绑定到 25 美分所包含的值上。
比如代码中 Coin::Quarter(UsState::Alaska)
作为 coin
的值传入 value_in_cents
函数,最终值 UsState::Alaska
被绑定到变量 state
上。
匹配 Option
可以使用 match
表达式来处理 Option<T>
,从 Some
中取出内部的 T
值。
比如编写一个接收 Option<i32>
的函数,若其中有值存在,则将这个值加 1;若其中不存在值,则直接返回 None
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => {
println!("The result is None");
None
}
Some(i) => {
println!("The result is {}", i + 1);
Some(i + 1)
}
}
}
fn main() {
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
需要注意的是,匹配必须穷举所有的可能。尤其是 Option<T>
这个例子中,Rust 会强迫我们明确地处理值为 None
的情形。
简单控制流 if let
if let
能让我们通过一种不那么繁琐的语法结合使用 if
与 let
,处理那些只关心某一种匹配而忽略其他匹配的情况。
下面的代码会匹配一个 Option<u32>
的值,并只在值为 3 时执行代码:1
2
3
4
5
6
7fn main() {
let some_number = Some(3);
match some_number {
Some(3) => println!("three"),
_ => (),
}
}
为了满足 match
表达式穷尽性的要求,我们不得不在处理完 Some(3)
变体后额外加上一句 _ => ()
。
可以使用 if let
以一种更简单的方式实现上述代码:1
2
3if let Some(3) = some_number {
println!("three");
}
还可以在 if let
中搭配使用 else
:1
2
3
4
5
6
7
8fn main() {
let some_number = Some(8);
if let Some(3) = some_number {
println!("three");
} else {
println!("other number");
}
}