本文是 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
两种用于模式匹配的语法。