Rust学习笔记4 - 枚举与模式匹配

本文是 Rust 学习笔记系列第四篇,参考 Rust 指南的第 6 章,涉及枚举和模式匹配。

接下来我们会介绍「枚举」(enumerations, abbr. enums),通过列举所有可能的变种来定义的类型。首先会尝试定义和使用枚举。接下来会介绍一种特殊且有用的枚举 Option(表示有值或者无值)。然后会介绍如何使用 match 表达式来进行模式匹配。最后会介绍另一种用于枚举的方便简洁的机制 if let

枚举在许多语言中都有涉及,但是几乎在每个语言中承载的功能都有所不同。Rust 的枚举类似于函数式语言(比如 F#、OCaml、Haskell)的代数数据类型。

定义枚举

我们来考虑一种使用枚举的场景:IP 地址。IP 地址目前有两种格式:IPv4 和 IPv6。所以在程序中我们是可以将 IP 地址格式全部枚举出来的。

所有的 IP 地址都是 IPv4 或者 IPv6 其中一种。这使得 IP 很适合用枚举这种数据结构来表达。

我们来定义用于表示 IP 类型的枚举:

1
2
3
4
enum IPAddr {
V4,
V6,
}

枚举值

我们可以为枚举的两个变种创建实例:

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

枚举的变种在它自身的命名空间里,可以用 :: 操作符来访问到。

我们可以让函数参数接受枚举类型:

1
2
3
4
fn route(ip_kind: IPAddr) {}

route(four);
route(six);

使用枚举还有更多的好处。比如说我们需要一个表示 IP 地址的类型,按照目前为止介绍的内容,我们可以构造出类似这样的结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum 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"),
}

而实际上,我们可以用只使用枚举完成这样的定义:

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

let home = IPAddr::V4(String::from("127.0.0.1"));
let loopback = IPAddr::V6(String::from("::1"));

我们可以直接在枚举中为每个变种定义关联数据,而且每个变种的关联数据类型可以不同。比如,我们可以将 IPv4 的数据定义为 4 个 u8 类型。

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

let home = IPAddr::V4(127, 0, 0, 1);
let loopback = IPAddr::V6(String::from("::1"));

我们再看另一个例子:

1
2
3
4
5
6
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(u8, u8, u8),
}

这个例子定义了四种不同数据类型的变种:

  • Quit 没有关联数据
  • Move 有一个匿名结构体的关联数据
  • Write 有一个字符串的关联数据
  • ChangeColor 有三个整数的关联数据

这与直接定义如下四个结构体类似:

1
2
3
4
5
6
7
struct QuitMessage;
struct MoveMessage {
x: i32,
y: i32,
}
struct WriteMessage(String);
struct ChangeColorMessage(u8, u8, u8);

但是如果使用结构体,就代表这些值都是不同的类型,就比较难定义一个通用的函数来接受这些值了。

枚举还有另一个与结构体相似的地方:枚举也可以定义方法和关联函数。

1
2
3
4
5
6
impl Message {
fn call(&self) {}
}

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

接下来我们看标准库中非常常见且有用的枚举:Option

Option 枚举

Option 是标准库中定义的枚举。它表示一个值有或者没有,是一个非常常见的场景。表达出一个值的有无,对于编译器来说,就可以分析你是否处理了所有的情况;这个功能可以避免在其他语言中常出现的 null 值问题。

编程语言的设计通常被认为是一个语言要包含哪些特性,然而不要包含哪些特性也是很重要的。Rust 不包含其他语言的 null 值特性。null 是一个表示没有值的值。在那些包含 null 值的语言里,通常变量会一直有两个状态:null 或者非 null。

null 值存在一个问题,如果你尝试把一个 null 值当作非 null 值来使用,会导致程序出错。而因为 null 值的普遍使用,使得这样的问题非常容易出现。

但是 null 值所表达的概念仍然很有用:一个值因为某些原因现在是无效的或者没有的。而问题不在于这个概念,而是具体的实现方式。因此 Rust 并不包含 null 值特性,而是用一个枚举来表达:

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

Option<T> 枚举非常常用,因此它被预置在了环境里,不需要自己引入。此外,它的变种 SomeNone 也不需要 Option:: 前缀便可以使用。Option<T> 依然是一个标准的枚举,而 Some(T)None 依然是枚举的变种。

<T> 是一个现在还没有介绍的特性:范型类型参数。现在,只需要知道它意味着 Some 的关联数据可以放入任何类型的数据。

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

let absent_number: Option<i32> = None;

如果我们使用 None,我们需要告诉编译期它是什么类型的,因为编译期无法推断出具体的范型类型。

因为 Option<T>T 是两种不同的类型,Option<T> 并不能直接当作 T 来使用,这意味着我们需要判断 Option<T> 的值是哪一个变种。

现在,你不需要再担心 null 值问题了,因为如果需要用到 null 值,你必须显式使用 Option<T> 枚举,而在其他情况,根本不需要担心 null 值会出现。

如果要使用 Option<T>,就意味着你需要判断它的值到底值哪个变种,你会需要在它是 Some(T) 的情况下,取出 T 的值并使用,也会需要在它是 None 时,做一些处理。match 表达式是一种用于枚举的控制流程结构。

match 控制流程操作符

Rust 的 match 表达式是一种功能非常强大控制流程,允许我们对一个值进行多种模式的比较,然后执行匹配的模式所对应的代码。模式可以是字面量,变量名,通配符等。后面会详细介绍每一种模式。match 强大的功能来自模式的表达能力,以及编译器会保证你要覆盖所有的可能性。

可以把 match 表达式想像成硬币分类器:硬币会跟随轨道向下滑动,并且会在第一个合适的洞掉下去。与之类似,match 会寻找第一个匹配的模式,然后所比较的值会填进这个模式,并执行对应的代码块。

我们可以用 match 来模拟硬币分类器的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
enum Coin {
Penny, // 1美分
Nickel, // 5美分
Dime, // 10美分
Quarter, // 25美分
}

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

绑定值的模式

模式的另一个有用的特性是它可以从匹配的值中提取部分值。

还是上面的例子,25 美分在不同的州是不一样的,我们可以尝试读取出它的州:

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
#[derive(Debug)] // Debug用于打印名字
enum UsState {
Alabama,
Alaska,
// --snip--
}

enum Coin {
Penny, // 1美分
Nickel, // 5美分
Dime, // 10美分
Quarter(UsState), // 25美分
}

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
}
}
}

匹配 Option<T>

上面我们介绍了 Option 枚举,我们需要在当它有值时,取出其中的值来使用;在它没有值时,做一些其他操作。

我们写一个函数,接收一个 Option<i32>,在当它有值时,给值加一,并返回包含新值的 Option

我们可以发现,用 match 能够以很简洁的代码写出这个函数:

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

匹配总是覆盖所有情况

编译器会向我们保证 match 表达式总是能覆盖所有情况,如果我们只覆盖了部分情况,则会产生编译错误。

1
2
3
4
5
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}
1
2
3
error[E0004]: non-exhaustive patterns: `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 `std::option::Option<i32>`

_ 占位符

当我们不想遍历所有情况时,可以用 Rust 提供的通配模式。比如我们只想在 u8 值为 1, 3, 5, 7 时做一些操作,我们可以用 _ 来表示所有其他情况:

1
2
3
4
5
6
7
8
let value = 0u8;
match value {
1 => println!("one"),
3 => println!("three"),
5 => println!("five"),
7 => println!("seven"),
_ => (),
}

然而,如果我们只想在一种模式匹配时执行一些代码,match 就有点累赘了,这时候我们可以选择用 if let

if let 进行简练的流程控制

if let 语法允许我们将 iflet 以一种简练的方式组合起来,让我们在符合某种模式时处理代码,并且忽略剩余的情况,比如上面那个 u8 的例子:

1
2
3
4
5
6
7
8
9
10
11
12
let value = Some(0u8);

// match
match value {
Some(3) => println!("three"),
_ => ()
}

// if let
if let Some(3) = value {
println!("three");
}

我们再考虑上面那个分硬币的例子,如果我们在找到 25 美分时通知别人,而其他的硬币都计入计数,我们可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
let mut count = 0;

match coin {
Coin::Quarter(state) => println!("State quarter from {:?}!", state),
_ => count += 1,
}

if let Coin::Quarter(state) = coin {
println!("State quarter from {:?}!", state);
} else {
count += 1;
}

总结

我们介绍了枚举的用法,并且介绍了使用 Option<T> 枚举来处理其他语言里 null 值的情况。

介绍了 matchif let 两种用于模式匹配的语法。

# Rust
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×