【Rust】枚举和模式匹配
在 Rust
中,枚举也可以包含数据,甚至是不同类型的数据。例如,Rust
的 Result<String, io::Error>
类型是一个枚举,这样的值要么是包含字符串的 Ok
值,要么是包含 io::Error
的 Err
值。
只要 value
只有一种可能,枚举就很有用。使用它们的代价是你必须安全地访问数据,使用模式匹配就可以完成。Rust
模式有点像正则表达式,它们用于检测一个值是否是想要的,他们也可以将结构体或tuple
中的多个字段提取到局部变量中。
枚举
来看一个标准库中枚举示例 std::cmp::Ordering
,它有三种可能的值:Ordering::Less
, Ordering::Equal
和 Ordering::Greater
,称为变量或者构造函数:
1 |
|
我们在使用的时候可以直接导入:
1 | use std::cmp::Ordering; |
如果导入当前模块的枚举的构造函数可以使用 self
:
1 | enum Pet { |
在内存中,C
风格的枚举值存储为整数,默认情况下,Rust
使用可以容纳它们的最小内置整数类型来存储 C
风格的枚举,大多是 1
字节,从 0
开始,但是也可以指定:
1 | enum HttpStatus { |
但是像上面的 404
,1 byte
就不能容纳了:
1 | use std::mem::size_of; |
可以使用 #[repr]
属性覆盖 Rust
的选择,就像上面的 Ordering
。
允许转换一个 C
风格的枚举值为整数,但是不能从整数转换为枚举值:
1 | assert_eq!(HttpStatus::Ok as i32, 200); |
与 C
和 C++
不同,Rust
保证枚举值只是枚举声明中拼写的值之一。从整数类型到枚举类型的未经检查的强制转换可能会破坏此保证,因此是不允许的。可以编写自己的转换函数:
1 | fn http_status_from_u32(n: u32) -> Option<HttpStatus> { |
或者使用 enum_primitive
。
像结构体一样,可以让 enum
自动派生内置 trait
,实现比较等运算符:
1 |
|
与其他语言不同的是,Enums
可以有自己的方法,就像结构体:
1 | impl TimeUnit { |
枚举数据
枚举值可以带带参数,第一种就像 tuple
结构体,这些枚举值就像构造函数一样可以创建枚举变量:
1 | enum RoughTime { |
第二种枚举值就像结构体,参数可命名:
1 | enum Shape { |
总之,Rust
有 3
种枚举,与 3
种 struct
相呼应,没有数据的枚举对应于类似 unit
结构体。单个枚举可以同时拥有这3
种类型:
1 | enum RelationshipStatus { |
内存表示
在内存中,带有数据的枚举被存储为一个小的整数标签,加上足够的内存来保存最大变体的所有字段。tag
字段供 Rust
内部使用,它告诉哪个构造函数创建了该值以及它具有哪些字段。
然而,Rust
没有对枚举布局做出任何承诺,以便为未来的优化敞开大门。在某些情况下,打包枚举的效率可能比图中所示的要高。
Json
示例
我们来看如何在代码中表示 JSON
数据,JSON
一共有 6
中数据类型:NULL
,Boolean(bool)
,Number(f64)
,String(String)
,Array(Vec<Json>)
,和 Object(Box<HashMap<String, Json>>)
:
1 | use std::collections::HashMap; |
关于 json
解析,可以查看 serde_json,crates.io
上最流行的库。
代表 Object
的 HashMap
周围的 Box
仅用于使所有 Json
值更紧凑,在内存中,Json
类型的值占用了 4
个机器字。 String
和 Vec
值是3
个字节,Rust
增加了1
个标记字节。Null
和 Boolean
值中没有足够的数据来用完所有空间,但所有 Json
值必须具有相同的大小,多余的空间未被使用。下图展示了 Json
值在内存中的布局,Box<HashMap>
是一个字:它只是一个指向堆分配数据的指针,我们通过 Box
使 Json
更加紧凑。
泛型枚举
枚举也可以是泛型的,这里有两个常用的例子 Option
和 Result
,和结构体的语法比较相似:
1 |
|
当类型 T
是引用、Box
或其他智能指针类型时,Rust
可以消除 Option<T>
的 tag
字段。 由于这些指针类型都不允许为零,Rust
可以将 Option<Box<i32>>
表示为单个机器字:0
表示无,非零表示 Some
指针。这使得此类 Option
类型非常类似于可能为空的 C
或 C++
指针值。不同之处在于,Rust
的类型系统要求在使用其内容之前检查选项是否为 Some
,这就避免了解引用空指针。
只需几行代码即可构建通用数据结构:
1 |
|
这几行代码定义了一个可以存储任意数量的 T
类型值的 BinaryTree
,每个 BinaryTree
要么为空要么不为空。如果是空的,那么什么数据都不包,如果不为空,那么它有一个 Box
,包含一个指向堆数据的指针。
每个 TreeNode
值包含一个实际元素,以及另外两个 BinaryTree
值。这意味着树可以包含子树,因此 NonEmpty
树可以有任意数量的后代。BinaryTree<&str>
类型值的示意图如下图所示。与 Option<Box<T>>
一样,Rust
消除了 tag
字段,因此 BinaryTree
值只是一个机器字。
构建这样一棵树可以用如下代码完成:
1 | use self::BinaryTree::*; |
大树可以由小树构成:
1 | let mars_tree = NonEmpty(Box::new(TreeNode { |
枚举的缺点是访问里面的数据必须使用 match
模式匹配。
模式匹配
假设有一个 RoughTime
值,需要访问值内的 TimeUnit
和 u32
字段。Rust
不允许直接 rough_time.0
和 rough_time.1
直接访问它们,因为值可能是 RoughTime::JustNow
,必须使用 match
:
1 | enum RoughTime { |
匹配枚举、结构体或元组就像 Rust
正在做一个简单扫描一样,依次检查每个 pattern
是否匹配。一个模式包含一些表示符,就像 count
和 units
,匹配之后,枚举值内容都会被移动会复制到这些局部变量中,这些局部变量只能在当前模式中使用。
Rust
的模式匹配除了匹配枚举值,还能匹配很多类型的数据,如下表所示:
字面量、变量、通配符
数值,字符,bool,字符串都可以用于模式匹配,例如:
1 | match meadow.count_rabbits() { |
这里的 n
和 other
都用于匹配其他的情况,可以使用 _
捕获剩余所有情况:
1 | let caption = match photo.tagged_pet() { |
要注意的是,Rust
中,必须为 match
列出所有可能情况,_
通常用来处理剩余的情况。
tuple
、结构体匹配
tuple
模式匹配元组,当你想要在单个匹配中获取多条数据时,它们很有用:
1 | fn describe_point(x: i32, y: i32) -> &'static str { |
结构体模式使用花括号,就像结构体表达式一样。 它们包含每个字段的子模式:
1 | match balloon.location { |
在这个例子中,如果第一个模式匹配上,那么 balloon.location.y
将存储在 height
中。像 Point { x: x, y: y }
这样的模式在匹配结构体时很常见,冗余的名称在视觉上很混乱,所以 Rust
有一个简写:Point {x, y}
, 意思是一样的。这种模式仍然将 Point
的 x
字段存储在新的本地 x
中,并将其 y
字段存储在新的本地 y
中。
即使使用简写,当我们只关心几个字段时,匹配一个大型结构也很麻烦:
1 | match get_account(id) { |
为了避免这个,可以使用 ..
去告诉 Rust
不要关心剩余的字段:
1 | Some(Account { name, language, .. }) => |
数组、切片匹配
模式可以匹配数组,例如:
1 | fn hsl_to_rgb(hsl: [u8; 3]) -> [u8; 3] { |
切片模式类似数组,不同的是切片具有可变长度,因此切片模式不仅匹配值,还要匹配长度,..
用于匹配任意数量的元素:
1 | fn greet_people(names: &[&str]) { |
ref
和 &
匹配不可复制的值会转移所有权,例如下面这段代码编译失败:
1 | match account { |
在这里,字段 account.name
和 account.language
被移动到局部变量 name
和 language
中,的其余部分被删除,这就是为什么我们不能在之后借用它。如果 name
和 language
都是可复制的值,Rust
会复制字段而不是移动它们,这段代码就可以了。但是如果这些是字符串,就需要一种借用匹配值而不是移动它们的模式,ref
关键字就是这样做的:
1 | match account { |
现在局部变量 name
和 language
是对 account
中相应字段的引用,由于 account
只是被借用而不是被消耗,因此可以继续对其调用方法。可以使用 ref mut
借用 mut
引用:
1 | match line_result { |
模式 Ok(ref mut line)
匹配任何成功结果,并借用 mut
引用存储在其中的成功值。
之前我们都是匹配的值,现在假如我们要匹配一个引用,假设 sphere.center()
返回 Point3d
的地址,例如 &Point3d { x: 0.0, y: 0.0, z: 0.0 }
,我们就得这样做:
1 | match sphere.center() { |
要记住的是,模式和表达式是自然对立的。表达式 (x, y)
将两个值组合成一个新的元组,但模式 (x, y)
则相反,它匹配一个元组并分解这两个值,&
也一样,在表达式中,&
创建一个引用,在一个模式中, &
匹配一个引用。
匹配引用遵循我们所期望的所有规则,无法通过共享引用获得 mut
访问权限。当我们匹配 &Point3d { x, y, z }
时,变量 x
、y
和 z
接收坐标的副本,而原始 Point3d
值保持不变。它之所以有效,是因为这些字段是可复制的。如果我们在具有不可复制字段的结构上尝试同样的事情,我们会得到一个错误:
1 | match friend.borrow_car() { |
但是我们可以使用 ref
获得对他的引用:
1 | Some(&Car { ref engine, .. }) => // ok, engine is a reference |
让我们再看一个 &
模式的例子。假设我们对字符串中的字符有一个迭代器 chars
,并且它有一个方法 chars.peek()
,它返回一个 Option<&char>
:对下一个字符的引用。程序可以使用 &
模式来获取指向的字符:
1 | match chars.peek() { |
条件模式
可以在 pattern
和 =>
之间使用 if CONDITION
来决定是否匹配,例如::
1 | match point_to_hex(click) { |
如果匹配成功,但是条件未达到,就会继续匹配下一个。
匹配多种可能
竖线 |
可用于在单个匹配中组合多个模式:
1 | let at_end = match chars.peek() { |
在表达式中,|
是按位或运算符,但在这里它更像正则表达式中的 |
,chars.peek()
匹配任何三种模式之一都会返回 true
。另外可以使用 ..=
匹配整个范围的值,范围模式包括开始和结束值,所以 '0' ..= '9'
匹配所有 ASCII
数字:
1 | match next_char { |
Rust
目前不允许在模式中使用 0..100
这样的不包含结束符的范围。
@
绑定
使用 x @ pattern
在匹配成功时会创建一个变量,将匹配到的整个值 copy
进去或者移动所有权,看这样一个示例代码:
1 | match self.get_selection() { |
第一个例子,解构 Shape::Rect
然后又构建了一个,我们可以使用 @
来完成这个目的:
1 | rect @ Shape::Rect(..) => { |
@
也用于范围绑定:
1 | match chars.next() { |
模式的其他用途
模式匹配除了应用于 match
,也可以应用与 tuple
,struct
以及 HashMap
解构:
1 |
|
还可以应用于我们之前学习到的 if let
和 while let
模式:
1 | // ...handle just one enum variant specially |