Rust枚举深度解析:构建灵活的数据结构

在计算机领域,总和类型(sum type)长期悲剧性缺位,很多事情却依然行得通,这简直不可思议(参见 Lambda 的缺位)。

——Graydon Hoare

Lambda 在主流编程领域的长期缺位造就了大量“烂”代码,总和类型的缺位同样如此

长期以来被 ML 社区和 Haskell 社区的黑客们称为总和类型、可区分的联合体(union)或代数数据类型。在 Rust 中被称为枚举

定义枚举

Rust 使用 enum 关键字定义枚举类型,例如,定义一个名为 Color 的类型,其值为 RedOrangeYellow 等

1
2
3
4
5
enum Color {
Red,
Orange,
Yellow
}

这声明了一个具有 3 个可能值的 Color 类型,称为变体构造器

使用枚举

创建枚举实例,使用 match 表达式,基于枚举变体进行操作

1
2
3
4
5
6
7
8
let c1 = Color::Red;
let c2 = Color::Orange;

match c1 {
Color::Red => println!("Red"),
Color::Orange => println!("Orange"),
Color::Yellow => println!("Yellow")
}

带数据的枚举

带数据的枚举允许在每个枚举变体上附加一个或多个值。这些值可以是任何类型,包括基础类型、复合类型,甚至其他枚举类型

1
2
3
4
5
6
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32)
}

Message 枚举中

  • Quit 是一个没有关联数据的枚举变体
  • Move 是一个带有两个整数字段 x 和 y 的枚举变体
  • Write 是一个带有一个字符串字段的枚举变体
  • ChangeColor 是一个带有三个整数字段的枚举变体,代表RGB颜色值

使用带数据的枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let quit_message = Message::Quit;
let move_message = Message::Move { x: 3, y: 4 };
let write_message = Message::Write("Hello".to_string());
let change_color_message = Message::ChangeColor(255, 0, 0);

match quit_message {
Message::Quit => println!("Quit"),
Message::Move { x, y } => println!("Move ({}, {})", x, y),
Message::Write(text) => println!("Write: {}", text),
Message::ChangeColor(r, g, b) => println!("ChangeColor RGB({}, {}, {})", r, g, b)
}

match move_message {
Message::Quit => println!("Quit"),
Message::Move { x, y } => println!("Move ({}, {})", x, y),
Message::Write(text) => println!("Write: {}", text),
Message::ChangeColor(r, g, b) => println!("ChangeColor RGB({}, {}, {})", r, g, b)
}
...

枚举也可以有方法,像结构体一样,使用 impl 关键字,基于 new 方法创建的 Message 实例

1
2
3
4
5
6
7
8
9
10
11
12
impl Message {
...

fn handle_quit(&self) -> Message {
// 处理 Quit 逻辑
}
}

match quit_message {
Message::Quit => Message::new().handle_quit(),
...
}

带数据的枚举在Rust中非常有用,因为允许在单个类型中封装多种不同的行为或状态,同时保持类型安全。经常用于表示命令、事件、消息或其他需要关联数据的等场景

内存中的枚举

在内存中,带有数据的枚举会以一个小型整数标签加上足以容纳最大变体中所有字段的内存块的格式进行存储。标签字段供 Rust 内部使用。它会区分由哪个构造器创建了值,进而决定这个值应该有哪些字段

1
2
3
4
5
enum RoughTime {
InThePast(TimeUnit, u32),
JustNow,
InTheFuture(TimeUnit, u32),
}

从 Rust 1.50 开始,RoughTime 会占用 8 字节,如图

这里可以对枚举在内存的情况有个大概了解,看不懂可以直接过,一般不影响实战开发

用枚举表示富数据结构

枚举对于快速实现树形数据结构也很有用。假设一个 Rust 程序需要处理任意 JSON 数据。在内存中,任何 JSON 文档都可以表示为这种 Rust 类型的值:

1
2
3
4
5
6
7
8
9
10
use std::collections::HashMap;

enum Json {
Null,
Boolean(bool),
Number(f64),
String(String),
Array(Vec<Json>),
Object(Box<HashMap<String, Json>>),
}

JSON 标准指定了可以出现在 JSON 文档中的不同数据类型:null、布尔值、数值、字符串、各种 JSON 值的数组以及具有字符串键名和 JSON 值的对象

serde_json 是 Rust 的结构体序列化库,是 crates.io 上最常下载的 crate 之一

接口参数,复杂参数一般标配 JSON

这里在表示 Object 的 HashMap 周围加 Box 只是为了让所有 Json 值更紧凑。在内存中,Json 类型的值占用 4 个机器字。而 String 值和 Vec 值占用 3 个机器字,Rust 又添加了一个标签字节。Null 值和 Boolean 值中没有足够的数据来用完所有空间,但所有 Json 值的大小必须相同。因此,额外的空间就用不上了。下图展示了 Json 值在内存中的实际布局的一些示例

HashMap 则更大。如果必须在每个 Json 值中为它留出空间,那么将会非常大,在 8 个机器字左右。但是 Box<HashMap> 是 1 个机器字:它只是指向堆中分配的数据的指针。甚至可以通过装箱更多字段来让 Json 更加紧凑

以下是一个表示JSON对象的例子

1
2
3
4
5
let json = Json::Object(vec![
("name".to_string(), Json::String("张三".to_string())),
("age".to_string(), Json::Number(30.0)),
("is_student".to_string(), Json::Bool(false)),
]);

泛型枚举

泛型枚举可以接受一个或多个类型参数。如 Rust 标准库中的两个例子

1
2
3
4
5
6
7
8
9
enum Option<T> {
Some(T),
None,
}

enum Result<T, E> {
Ok(T),
Err(E),
}

Option枚举有两个变体:Some 和 None。Some 变体包含一个值,这个值的类型是T。T是一个类型参数,它可以是任何类型。使用Option枚举

1
2
let some_number: Option<i32> = Some(5);
let no_number: Option<i32> = None;

枚举相关内容就这么多了,不同类型的枚举,如何使用,基本操作都已经清楚了,接下来是 Rust 中的 模式

欢迎大家讨论交流,如果喜欢本文章或感觉文章有用,动动你那发财的小手点赞、收藏、关注再走呗 ^_^ 

微信公众号:草帽Lufei
掘金:草毛lufei