枚举和模式匹配
本文大部分内容翻译自:The Rust Programming Language
枚举(enumerations),也被称作enums
,允许你通过列举可能的成员
(variants)来定义一个类型。
1. 枚举的定义
枚举是除结构体之外,另一种定义自定义数据类型的方式,它与结构体定义自定义类型的方式不同。
例:需要使用IP地址,由于只有IPv4和IPv6两种类型,所以可以枚举这两种可能的类型。任何一个IP地址只可能是IPv4和IPv6中的一个,不可能两者都是,所以枚举很适合处理这种情况,因为一个枚举值也只能是该类型的所有成员中的一个。
1 | enum IpAddrKind { |
现在IpAddrKind
就是一个可以在代码中使用的自定义数据类型了。
1.1. 枚举值
创建IpAddrKind
两个不同成员的实例:
1 | let four = IpAddrKind::V4; |
four
和six
,或者IpAddrKind::V4
和IpAddrKind::V6
是同一种类型——IpAddrKind
。之后我们就可以定义一个参数为IpAddrKind
的函数:
1 | fn route(ip_kind: IpAddrKind) {} |
任一成员都可以调用route
函数:
1 | route(IpAddrKind::V4); |
不但定义类型,还定义与类型绑定的值:
1 | fn main() { |
IpAddr
enum says that both V4 and V6 variants will have associated String
values.
上面定义的每个枚举成员也变成了可以构造枚举实例的函数(function)。
例如:IpAddr::V4()
是一个获取String
类型的参数、返回IpAddr
类型的函数。
不同枚举成员可以有不同类型的数据与它们关联:
1 | enum IpAddr { |
还可以为不同枚举成员传入不同的struct参数:
1 | struct Ipv4Addr { |
可以将任意类型的数据放入枚举成员中,甚至可以存入另一种枚举类型。
1.2. 定义与枚举类型关联的方法
1 | fn main() { |
1.3. 枚举类型:Option
Option是标准库定义的一个enum。Option类型应用广泛因为它编码了一个非常普遍的场景,即一个值要么有值要么没值。
Rust中并没有null
,不过它有一个可以编码存在或不存在概念的枚举。这个枚举是Option<T>
:
1 | enum Option<T> { |
Optionprelude
中,因此不需要将它显式引入作用域。它的成员也是如此,不需要用Option::
前缀,可以直接使用Some
和None
。
<T>
语法是一个泛型类型参数(generic type parameter)。目前只需要知道:
<T>
意味着Option
枚举的Some
成员可以包含任意类型的数据- 用在
T
位置的具体类型的不同,使Option<T>
这个整体也变成不同的类型
例:
1 | let some_number = Some(5); |
some_number
的类型是Option<i32>
;some_string
的类型是Option<&str>
,它们是不同的类型。
对于absent_number
,我们指定了它的类型:Option<i32>
,因为编译器无法推断像推断Some
成员那样推断出None
是什么类型。
- 当我们有一个
Some
值,我们就知道一个值是存在的,并且被保存在Some
中; - 当我们有一个
None
值时,在某种意义上,它跟空值(null)具有相同的意义:并没有一个有效的值。
那么,Option<T>
为什么就比空值要好呢?
- 简言之,因为
Option<T>
和T
(这里T
可以是任何类型)是不同的类型,编译器不允许像使用一个肯定有效的值那样使用 Option。例如: 会得到编译错误:1
2
3
4let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y;编译器不允许将1
2
3
4
5
6
7
8
9
10
11
12$ cargo run
Compiling enums v0.1.0 (file:///projects/enums)
error[E0277]: cannot add `Option<i8>` to `i8`
--> src/main.rs:5:17
|
5 | let sum = x + y;
| ^ no implementation for `i8 + Option<i8>`
|
= help: the trait `Add<Option<i8>>` is not implemented for `i8`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `enums` due to previous errorOption<i8>
与i8
相加。 - 当在 Rust 中拥有一个像
i8
这样类型的值时,它一定是有一个有效的值,我们可以自信地使用它,无需做空值检查。只有当使用Option<i8>
(或者任何用到的类型)的时候需要担心可能没有值。这时,编译器会确保我们在使用这样的值之前处理了为空的情况。 - 换句话说,在对
Option<T>
进行T
的运算之前,必须将其转换为T
。这可以帮助我们捕获到空值最常见的问题之一:假设某值不为空但实际上为空的情况。
想要处理一个可能为空的值,你必须要明确地将它转换成Option<T>
类型。这样,当使用这个值时,就需要明确地处理值为空的情况。所以,只要一个值不是Option<T>
类型,你都可以安全地认为这个值不是null
。
这是Rust一个经过深思熟虑的设计决策,来限制空值的泛滥以增加Rust代码的安全性。
为了使用Option<T>
类型的值,就需要去处理每个枚举成员(Some、None):
- 需要处理遇到
Some(T)
的情况,并允许使用其中的T
- 需要处理遇到
None
的情况,这种情况下没有可用的T
值
match
表达式就是处理枚举成员的控制流结构:它会根据枚举成员的不同去运行不同的代码,并且这些代码中可以使用被匹配到的值。
2. match控制流结构
例:
1 | enum Coin { |
match
关键字后面跟一个表达式(expression)- 与
if
不同的是,if
后面的表达式需要返回Boolean类型,而match
可以返回任何类型 - arms(分支),包括:
- a pattern
- some code:是表达式,表达式的结果就是
match
会返回的结果
使用=>
运算符分隔开。
- 分支之间用
,
分隔
2.1. 绑定了值的pattern
1 | // so we can inspect the state in a minute |
我们在匹配Coin::Quarter
成员的分支的模式中增加了一个叫做state
的变量。当匹配到Coin::Quarter
时,变量state
将会绑定25美分硬币所对应州的值,接着在那个分支的代码中使用state
:
1 |
|
2.2. 匹配Option
1 | fn main() { |
2.3. 匹配时穷尽的
下面这段代码有bug,不能编译:
1 | fn main() { |
我们没有处理None
的情况。编译错误:
1 | $ cargo run |
我们必须穷举到最后一种可能才能使代码有效。
2.4. 通配模式(Catch-all Patterns)和_
占位符
1 | fn main() { |
必须将通配分支放在最后,因为模式是按顺序匹配的,如果在通配分支后添加其他分支,Rust会发出警告,因为通配分支后的分支永远不会被匹配到。
Rust还提供了一个模式。当我们不想使用通配模式获取的值时,可以使用_
。这是一个特殊的模式,可以匹配任意值而不绑定到该值。这告诉Rust我们不会使用这个值,所以Rust也不会警告我们代码中存在未使用的变量。
举例:
1 | fn main() { |
如果在掷出3和7以外的数时,想要什么也不做,可以返回()
:
1 | fn main() { |
3. if let
简洁控制流
3.1. if let
if let
语法让我们以一种不那么冗长的方式结合if
和let
,来处理“只匹配一个模式的值、忽略其他模式”的情况。
只处理Some
成员:
1 | fn main() { |
为了满足match
表达式“穷尽性”的要求,必须在处理完Some
这个唯一的成员后加上_ => ()
,这样就要增加很多烦人的样板代码。
这种情况下,可以使用更简洁的if let
:
1 | fn main() { |
语法是:
1 | if let pattern = expression { |
用于匹配的pattern与被匹配的expression之间用=
隔开,如果匹配上了,执行花括号内的代码。
3.2. if let
与else
联用
else
代码块的作用跟match
中_
分支一样。
使用match
:
1 | let coin = Coin::Penny; |
使用if let
和else
:
1 | let coin = Coin::Penny; |
参考资料
[1] 枚举和模式匹配:https://kaisery.github.io/trpl-zh-cn/ch06-00-enums.html