本文是 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  | enum IPAddr { | 
枚举值
我们可以为枚举的两个变种创建实例:
1  | let four = IPAddr::V4; | 
枚举的变种在它自身的命名空间里,可以用 :: 操作符来访问到。
我们可以让函数参数接受枚举类型:
1  | fn route(ip_kind: IPAddr) {} | 
使用枚举还有更多的好处。比如说我们需要一个表示 IP 地址的类型,按照目前为止介绍的内容,我们可以构造出类似这样的结构体:
1  | enum IPAddrKind { | 
而实际上,我们可以用只使用枚举完成这样的定义:
1  | enum IPAddr { | 
我们可以直接在枚举中为每个变种定义关联数据,而且每个变种的关联数据类型可以不同。比如,我们可以将 IPv4 的数据定义为 4 个 u8 类型。
1  | enum IPAddr { | 
我们再看另一个例子:
1  | enum Message { | 
这个例子定义了四种不同数据类型的变种:
Quit没有关联数据Move有一个匿名结构体的关联数据Write有一个字符串的关联数据ChangeColor有三个整数的关联数据
这与直接定义如下四个结构体类似:
1  | struct QuitMessage; | 
但是如果使用结构体,就代表这些值都是不同的类型,就比较难定义一个通用的函数来接受这些值了。
枚举还有另一个与结构体相似的地方:枚举也可以定义方法和关联函数。
1  | impl Message { | 
接下来我们看标准库中非常常见且有用的枚举:Option。
Option 枚举
Option 是标准库中定义的枚举。它表示一个值有或者没有,是一个非常常见的场景。表达出一个值的有无,对于编译器来说,就可以分析你是否处理了所有的情况;这个功能可以避免在其他语言中常出现的 null 值问题。
编程语言的设计通常被认为是一个语言要包含哪些特性,然而不要包含哪些特性也是很重要的。Rust 不包含其他语言的 null 值特性。null 是一个表示没有值的值。在那些包含 null 值的语言里,通常变量会一直有两个状态:null 或者非 null。
null 值存在一个问题,如果你尝试把一个 null 值当作非 null 值来使用,会导致程序出错。而因为 null 值的普遍使用,使得这样的问题非常容易出现。
但是 null 值所表达的概念仍然很有用:一个值因为某些原因现在是无效的或者没有的。而问题不在于这个概念,而是具体的实现方式。因此 Rust 并不包含 null 值特性,而是用一个枚举来表达:
1  | enum Option<T> { | 
Option<T> 枚举非常常用,因此它被预置在了环境里,不需要自己引入。此外,它的变种 Some 和 None 也不需要 Option:: 前缀便可以使用。Option<T> 依然是一个标准的枚举,而 Some(T) 和 None 依然是枚举的变种。
<T> 是一个现在还没有介绍的特性:范型类型参数。现在,只需要知道它意味着 Some 的关联数据可以放入任何类型的数据。
1  | let some_number = Some(5); | 
如果我们使用 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  | enum Coin { | 
绑定值的模式
模式的另一个有用的特性是它可以从匹配的值中提取部分值。
还是上面的例子,25 美分在不同的州是不一样的,我们可以尝试读取出它的州:
1  | #[derive(Debug)] // Debug用于打印名字 | 
匹配 Option<T>
上面我们介绍了 Option 枚举,我们需要在当它有值时,取出其中的值来使用;在它没有值时,做一些其他操作。
我们写一个函数,接收一个 Option<i32>,在当它有值时,给值加一,并返回包含新值的 Option。
我们可以发现,用 match 能够以很简洁的代码写出这个函数:
1  | fn plus_one(x: Option<i32>) -> Option<i32> { | 
匹配总是覆盖所有情况
编译器会向我们保证 match 表达式总是能覆盖所有情况,如果我们只覆盖了部分情况,则会产生编译错误。
1  | fn plus_one(x: Option<i32>) -> Option<i32> { | 
1  | error[E0004]: non-exhaustive patterns: `None` not covered  | 
_ 占位符
当我们不想遍历所有情况时,可以用 Rust 提供的通配模式。比如我们只想在 u8 值为 1, 3, 5, 7 时做一些操作,我们可以用 _ 来表示所有其他情况:
1  | let value = 0u8; | 
然而,如果我们只想在一种模式匹配时执行一些代码,match 就有点累赘了,这时候我们可以选择用 if let
用 if let 进行简练的流程控制
if let 语法允许我们将 if 和 let 以一种简练的方式组合起来,让我们在符合某种模式时处理代码,并且忽略剩余的情况,比如上面那个 u8 的例子:
1  | let value = Some(0u8); | 
我们再考虑上面那个分硬币的例子,如果我们在找到 25 美分时通知别人,而其他的硬币都计入计数,我们可以这样写:
1  | let mut count = 0; | 
总结
我们介绍了枚举的用法,并且介绍了使用 Option<T> 枚举来处理其他语言里 null 值的情况。
介绍了 match 和 if let 两种用于模式匹配的语法。