在计算机领域,总和类型(sum type)长期悲剧性缺位,很多事情却依然行得通,这简直不可思议(参见 Lambda 的缺位)。
------Graydon Hoare
Lambda 在主流编程领域的长期缺位造就了大量"烂"代码,总和类型的缺位同样如此
长期以来被 ML 社区和 Haskell 社区的黑客们称为总和类型、可区分的联合体(union)或代数数据类型。在 Rust 中被称为枚举
定义枚举
Rust 使用 enum
关键字定义枚举类型,例如,定义一个名为 Color
的类型,其值为 Red
、Orange
、Yellow
等
Rust
enum Color {
Red,
Orange,
Yellow
}
这声明了一个具有 3 个可能值的 Color
类型,称为变体 或构造器
使用枚举
创建枚举实例,使用 match
表达式,基于枚举变体进行操作
rust
let c1 = Color::Red;
let c2 = Color::Orange;
match c1 {
Color::Red => println!("Red"),
Color::Orange => println!("Orange"),
Color::Yellow => println!("Yellow")
}
带数据的枚举
带数据的枚举允许在每个枚举变体上附加一个或多个值。这些值可以是任何类型,包括基础类型、复合类型,甚至其他枚举类型
rust
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32)
}
在 Message
枚举中
Quit
是一个没有关联数据的枚举变体Move
是一个带有两个整数字段x
和y
的枚举变体Write
是一个带有一个字符串字段的枚举变体ChangeColor
是一个带有三个整数字段的枚举变体,代表RGB颜色值
使用带数据的枚举
rust
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
实例
rust
impl Message {
...
fn handle_quit(&self) -> Message {
// 处理 Quit 逻辑
}
}
match quit_message {
Message::Quit => Message::new().handle_quit(),
...
}
带数据的枚举在Rust中非常有用,因为允许在单个类型中封装多种不同的行为或状态,同时保持类型安全。经常用于表示命令、事件、消息或其他需要关联数据的等场景
内存中的枚举
在内存中,带有数据的枚举会以一个小型整数标签加上足以容纳最大变体中所有字段的内存块的格式进行存储。标签字段供 Rust 内部使用。它会区分由哪个构造器创建了值,进而决定这个值应该有哪些字段
rust
enum RoughTime {
InThePast(TimeUnit, u32),
JustNow,
InTheFuture(TimeUnit, u32),
}
从 Rust 1.50 开始,RoughTime
会占用 8 字节,如图
这里可以对枚举在内存的情况有个大概了解,看不懂可以直接过,一般不影响实战开发
用枚举表示富数据结构
枚举对于快速实现树形数据结构也很有用。假设一个 Rust 程序需要处理任意 JSON 数据。在内存中,任何 JSON 文档都可以表示为这种 Rust 类型的值:
rust
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对象的例子
rust
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 标准库中的两个例子
rust
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
Option枚举有两个变体:Some 和 None。Some 变体包含一个值,这个值的类型是T。T是一个类型参数,它可以是任何类型。使用Option枚举
rust
let some_number: Option<i32> = Some(5);
let no_number: Option<i32> = None;
枚举相关内容就这么多了,不同类型的枚举,如何使用,基本操作都已经清楚了,接下来是 Rust 中的 模式
欢迎大家讨论交流,如果喜欢本文章或感觉文章有用,动动你那发财的小手点赞、收藏、关注再走呗
^_^
微信公众号:草帽Lufei