08 - 枚举与模式
8.1 - 枚举
-
枚举的定义:
enum Ordering { Less, Equal, Greater }
-
枚举的值被称为变体,或者构造式(constructor):
Ordering::Less
;Ordering::Equal
;Ordering::Greater
。
-
这个枚举是标准库中定义的,可以在代码中直接导入它本身:【推荐】
use std::cmp::Ordering;
-
或者,导入它的所有构造式:除非可以让代码更好理解,否则不要直接导入构造式。
use std::cmp::Ordering::*;
-
-
如果要导入当前模块中声明的枚举的构造式,要使用
self
参数:enum Pet { Orca, Giraffe } use self::Pet::*;
-
Rust 允许把枚举值转换为整数,但不能直接把整数转换为枚举值。
-
Rust 要保证枚举值一定是在
enum
声明中定义的。 -
枚举和结构体一样,支持使用
#[derive]
属性,来标记共有特型。 -
枚举和结构体一样,也可以通过
impl
块定义方法。
8.1.1 - 包含数据的枚举
-
枚举包含什么数据,就称为什么变体:
#[derive(Copy, Clone, Debug, PartialEq)] enum RoughTime { InThePast(TimeUnit, u32), JustNow, InTheFuture(TimeUnit, u32) }
InThePast
和InTheFuture
称为元组变体(tuple variant)- 枚举值也可以是结构体变体
RoughTme
的每个构造式占用 8 个字节。
-
Rust 有三种枚举变体:
-
类基元变体:没有数据的变体。
enum EnumName;
-
元组变体:
enum EnumName (...);
-
结构体变体:
enum EnumName { ... }
-
一个枚举中可以同时包含这三种变体数据:
enum RelationshipStatus { Single, InARelationship, ItsComplicated(Option<String>), ItsExtremelyComplicated { car: DifferentialEquation, cdr: EarlyModernistPoem } }
-
-
公有枚举的所有构造式和字段自动都是公有的。
8.1.2 - 枚举的内存布局
- 整数标签是 Rust 内部使用的字段,通过它可以知道是哪个构造式创建了当前的值,以及当前的值包含哪些字段。
- Rust 对某些枚举会直接优化掉标签字段。如泛型枚举。
8.1.3 - 使用枚举的富数据结构
-
枚举可以用来快速实现类似树的数据结构。
-
Rust 实现
Json
枚举:enum Json { Null, Boolean(bool), Number(f64), String(String), Array(Vec<Json>), Object(Box<HashMap<String, Json>>) }
- JSON 标准规定了可以出现在 JSON 文档中的不同数据类型有:null、布尔值、数值、字符串、JSON 值数组、以字符串为键以 JSON 值为值的对象。
- 用
Box
包装HashMap
来表示Object
,可以让所有 JSON 值更简洁。 - JSON 类型的值占 4 个机器字。
String
和Vec
值占 3 个机器字Box<HashMap>
占 1 个机器字
8.1.4 - 泛型枚举
-
常见的泛型枚举:
enum Option<T> { None, Some(T) } enum Result<T, E> { Ok(T), Err(E) }
-
基于泛型的数据结构举例:实现一个二叉树
// 创建T类型值的有序集合 enum BinaryTree<T> { // BinaryTree的值只占1个机器字 Empty, // 不包含任何数据 NonEmpty(Box<TreeNode<T>>) // 包含一个Box,它是指向位于堆内存的TreeNode的指针 } // BinaryTree的节点 struct TreeNode<T> { element: T, // 实际的元素 left: BinaryTree<T>, // 左子树 right: BinaryTree<T> // 右子树 }
-
创建这个树的任何特定节点:
use self::BinaryTree::* let jupiter_tree = NonEmpty(Box::new(TreeNode { element: "Jupiter", left: Empty, right: Empty }));
-
大一点的树可以基于小一点的树创建
// 将jupiter_node和mercury_node的所有权,通过赋值转移给新的父节点mars_tree let mars_tree = NonEmpty(Box::new(TreeNode { element: "Mars", left: jupiter_tree, right: mercury_tree }));
-
根节点也使用相同的方式创建:
let tree = NonEmpty(Box::new(TreeNode { element: "Saturn", left: mars_tree, right: uranus_tree }));
-
假如这个树有一个
add
方法,那么可以通过这样调用这个树:let mut tree = BinaryTree::Empty; for planet in planets { tree.add(planet); }
-
8.2 - 模式
-
访问枚举数据的唯一安全方式:模式匹配。
-
定义一个
RoughTme
枚举类型:enum RoughTime { InThePast(TimeUnit, u32), JustNow, InTheFuture(TimeUnit, u32) }
-
使用
match
表达式访问枚举的数据:fn rough_time_to_english(rt: RoughTime) -> String { match rt { RoughTime::InThePast(units, count) => format!("{} {} ago", count, units.plural()), RoughTime::JustNow => format!("just now"), RoughTime::InTheFuture(units, count) => format!("{} {} from now", count, units.plural()) } }
-
-
枚举、结构体或元组在匹配模式时,会从左到右堆模式的每个组件,依次检查当前值是否与之匹配。如果不匹配,就会进入到下一个模式。
-
模式的特型
-
Rust 模式本身就是一个迷你语言:
模式类型 示例 说明 字面量 100
“name”匹配确切的值;const 声明的名字也可以 范围 0 … 100
‘a’ … ‘k’匹配范围中的任意值,包括最终值 通配符 _ 匹配任意值并忽略该值 变量 name
mut count类似 _
,但会把匹配的值转移或复制到新的局部变量ref
变量ref field
ref mut field不转移或复制匹配的值,而是借用匹配值的引用 子模式绑定 val @ 0 … 99
ref circle @Shape::Circle { .. }
匹配 @右侧的模式,使用左侧的变量名 枚举模式 Some(value)
NonePet::Orca
元组模式 (Key, value)
<r, g, b>结构体模式 Color(r, g, b)
Point { x, y }
Card { suit: Clubs, rank: n }
Account { id, name, … }引用 &value
&(k, v)
只匹配引用值 多个模式 'a'
'A'
护具表达式 x if x * x <= r2
仅限 match(不能在 let 等中使用)
8.2.1 - 模式中的字面量、变量和通配符
-
0、1 等整数值可以作为模式使用:
match meadow.count_rabbits() { 0 => {}, 1 => println!("A rabbit is nosing around in the clover."), n => println!("There are {} rabbits hopping about in the meadow", n) }
-
其他类型的字面量也可以用作模式,包括布尔值、字符,甚至字符串:
let calendar = match settings.get_string("calendar") { "gregorian" => Calendar::Gregorian, "chinese" => Calendar::Chinese, "ethiopian" => Calendar::Ethiopian, other => return parse_error("calendar", other) };
-
可以使用通配符
_
作为模式,以匹配任意值,但不保存匹配的值:let caption = match photo.tagged_pet() { Pet::Tyrannosaur => "RRRAAAAAHHHHHH", Pet::Samoyed => "*dog thoughts*", _ => "I'm cute, love me" // 通用标题,任何宠物都适用 };
-
每个
match
表达式最后都会有一个通配符,即使非常确定其他情况不会发生,也必须至少加上一个后备的诧异分支:// 有很多形状(shape),但只支持“选择”某些文本, // 或者一个矩形区域中的所有内容,不能选择椭圆或梯形。 match document.selection() { Shape::TextSpan(start, end) => paint_text_selection(start, end), Shape::Rectangle(rect) => paint_rect_selection(rect), _ => panic!("unexpected selection type") }
-
为了避免最后一个分支无法运行到,可以结合
if
表达式实现模式:fn check_move(current_hex: Hex, click: Point) -> game::Result<Hex> { match point_to_hex(click) { None => Err("That's not a game space"), Some(hex) => if hex == current_hex { Err("You are already there! You must click somewhere else") } else { Ok(hex) } } }
8.2.2 - 元组与结构体模式
-
元组模式匹配元组,适合在一个
match
表达式中同时匹配多个数据:fn describe_point(x: i32, y: i32) -> &'static str { use std::cmp::Ordering::*; match (x.cmp(&0), y.cmp(&0)) { (Equal, Equal) => "at the orgin", (_, Equal) => "on the x axis", (Equal, _) => "on the y axis", (Greater, Greater) => "in the first quadrant", (Less, Greater) => "in the second quadrant", _ => "somewhere else" } }
-
结构体模式使用花括号,类似结构体表达式,其中的每个字段都是一个子模式:
match balloon.location { Point { x: 0, y: height } => println!("straight up {} meters", height), Point { x: x, y: y } => println!("at ({}m, {}m)", x, y) }
-
对于复杂的结构体,为了使代码简洁,可以使用
..
表示不关心其他字段:match get_account(id) { ... Some(Account { name, language, .. }) => language.show_custom_greeting(name) }
8.2.3 - 模式的引用
-
对于引用,Rust 支持两种模式:
ref
模式:借用匹配值的元素&
模式:匹配引用
-
一般情况下,匹配不可复制的值会转移值:
match account { Account { name, language, .. } => { ui.greet(&name, &language); ui.show_settings(&account); // 错误:使用了转移的值account } }
-
使用
ref
模式,可以借用匹配的值,而不转移它:match account { Account { ref name, ref language, .. } => { ui.greet(name, language); ui.show_settings(&account); } }
-
还可以用
ref mut
借用mut
引用:match line_result { Err(ref err) => log_error(err), // err是&Error(共享的ref) // 模式Ok(ref mut line)可以匹配任何成功的结果,并借用该结果中存储的值的mut引用 Ok(ref mut line) => { // line是&mut String(可修改的ref) trim_comments(line); // 就地修改字符串 handle(line); } }
-
与
ref
模式对应的是&
模式。以&
开头的模式匹配引用:match sphere.center() { &Point3d {x, y, z} => ... }
-
匹配引用遵循引用的规则:
- 生命期;
- 不能对共享引用采取
mut
操作; - 不能从引用(包括
mut
引用)中转移出值。
8.2.4 - 匹配的多种可能性
-
竖线
|
可用于在一个match
分支中组合多个模式:let at_end = match chars.peek() { Some(&'\r') | Some(&'\n') | None => true, _ => false };
-
使用
...
可以匹配某个范围中的值。范围模式包含起点值和终点值,即'0' ... '9'
匹配所有 ASCII 数字:match next_char { '0' ... '9' => self.read_number(), 'a' ... 'z' | 'A' ... 'Z' => self.read_word(), ' ' | '\t' | '\n' => self.skip_whitespace(), _ => self.handle_punctuation() }
-
全纳(inclusive)范围
...
:对模式匹配比较适用。 -
互斥范围
..
:对循环和片段比较适用。
8.2.5 - 模式护具
-
使用
if
关键字可以为match
分支添加护具。只有在护具求值为true
时匹配才成功,如果护具求值为false
,那么 Rust 会继续匹配下一个模式。match robot.last_known_location() { Some(point) if self.distance_to(point) < 10 => short_distance_strategy(point), Some(point) => long_distance_strategy(point), None => searching_strategy() }
8.2.6-@
模式
-
x @ pattern
可以匹配给定的pattern
,可以把匹配值整个转移或复制到一个变量x
中:-
创建如下的模式:
match self.get_selection() { Shape::Rect(top_left, bottom_right) => optimized_paint(&Shape::Rect(top_left, bottom_right)), other_shape => paint_outline(other_shape.get_outline()), }
-
Shape::Rect
分支拆解出值后,可以再重新创建一个相同的值,所以可以用@
模式重写:rect @ Shape::Rect(..) => Optimized_paint(&rect),
-
@
模式支持匹配范围值:match chars.next() { Some(digit @ '0' ... '9') => read_number(digit, chars), ... }
-
8.2.7 - 模式的使用场景
-
通过模式匹配可以实现拆解值,而不是仅仅把值保存在一个变量中:
-
用在
match
表达式中 -
用来代替标识符
-
用于将结构体拆解为 3 个新的局部变量:
let Track { album, track_number, title, .. } = song;
-
用于拆解作为函数参数的元组:
fn distance_to((x, y): (f64, f64)) -> f64 { ... }
-
用于迭代
HashMap
的键和值:for (id, document) in &cache_map { println!("Document #{}: {}", id, document.title); }
-
用于自动对传给闭包的参数解引用:
let sum = numbers.fold(0, |a, &num| a + num);
-
-
上述例子,在 JavaScript 中叫解构(restructuring),Python 中叫解包(unpacking)。
-
不可驳模式(irrefutable pattern):始终都可以匹配的模式。可用于:
let
后面。- 函数参数中。
for
后面。- 闭包参数中。
-
可驳模式(refutable pattern):可能不会匹配的模式。可用于:
-
match
表达式。 -
if let
表达式。 -
while let
表达式。 -
只处理一种特定的枚举变体:
if let RoughTime::InTheFuture(_, _) = user.date_of_birth() { user.set_time_traveler(true); }
-
只在查表成功时运行某些代码:
if let Some(document) = cache_map.get(&id) { return send_cached_response(document); }
-
不成功则重复做一些事:
while let Err(err) = present_cheesy_anti_robot_task() { log_robot_attempt(err); }
-
手工便利一个迭代器:
while let Some(_) = lines.peek() { read_paragraph(&mut lines); }
-
8.2.8 - 填充二叉树
实现 BinaryTree::add()
方法,用于向 BinaryTree
中添加相同类型的子节点:
enum BinaryTree<T> {
Empty,
NonEmpty(Box<TreeNode<T>>)
}
struct TreeNode<T> {
element: T,
left: BinaryTree<T>,
right: BinaryTree<T>
}
impl<T: Ord> BinaryTree<T> {
fn add(&mut self, value: T) {
match *self {
BinaryTree::Empty =>
*self = BinaryTree::NonEmpty(Box::new(TreeNode {
element: value,
left: BinaryTree::Empty,
right: BinaryTree::Empty
})),
BinaryTree::NonEmpty(ref mut node) =>
if value <= node.element {
node.left.add(value);
} else {
node.right.add(value);
}
}
}
}
-
当
*self
为空,那么运行BinaryTree::Empty
模式,把Empty
树改成NonEmpty
树。 -
当
*self
不为空,那么运行BinaryTree::NonEmpty(ref mut node)
模式,可以访问并修改该树节点中的数据。 -
调用这个
add
方法:let mut tree = BinaryTree::Empty; tree.add("Mercury"); tree.add("Venus"); ...
8.3 - 模式的设计
- Rust 的枚举,也被称为代数数据类型(algebraic data type)。与下述特点密切相关:
- 变体(variant)
- 引用
- 可变性(mutability)
- 内存安全
- 函数变成语言不需要可变性。
- C 的
union
联合体同时支持变体、指针(引用)和可变性,但它不是内存安全的。 - 枚举是特定数据形态的设计工具:
- 一个值可能是一个值;
- 也可能是另外一个值;
- 还有可能什么也不是
- 枚举不支持扩展枚举:
- 如果要添加一个新变体,那么只能修改枚举的声明
- 修改了枚举的声明,必须重新检查匹配中每个变体的每个
match
表达式,因为需要给他们都添加一个新分支以处理新变体。
- Rust 的特型比枚举更具有灵活性。
详见《Rust 程序设计》(吉姆 - 布兰迪、贾森 - 奥伦多夫著,李松峰译)第十章
原文地址