主题
Rust 深入理解 Enum(枚举)与 TypeScript 对比
如果说 Ownership 和 Borrowing 是 Rust 的“灵魂”,
那 enum(枚举)+ pattern matching(模式匹配) 就是它的“表达能力核心”。
在深入语法之前,先回答两个最关键的问题:
- Enum 到底是干什么的?
- 你在真实项目里会拿它做什么?
简单一句话概括:
Enum 用来把“有限但互斥的几种情况 / 形态”打包成一个类型,
让编译器帮你保证只会出现这些情况,并且所有情况都被你处理到了。
它可以解决这几类常见问题:
- 告别魔法数字 / 魔法字符串:不再到处写
"success"/"error"/0/1 - 提升可读性:一眼就能看出“系统可能有哪些状态 / 命令”
- 防止漏逻辑:
match+ enum 强制你处理所有分支,否则编译不过 - 更易重构:新增一种情况时,编译器会带你把所有相关逻辑都改一遍
几类典型用途:
- 业务状态建模:订单状态、支付状态、异步请求状态
- Rust:
enum OrderStatus { Created, Paid, Shipped, Cancelled } - TS:
type OrderStatus = "Created" | "Paid" | "Shipped" | "Cancelled"
- Rust:
- 操作 / 命令表达:编辑器命令、用户输入、游戏角色动作
- 你的
Command就是标准例子
- 你的
- 数据结构形态:
Option<T>、Result<T, E>、AST 节点、UI 节点等 - 状态机:页面生命周期、工作流步骤、协议握手阶段
在下面这段代码里,你可以先感受两个典型的 enum 场景:
ProductCategory:普通枚举Command:携带不同数据的枚举
这篇文章会:
- 从这两个例子出发,讲清楚 Rust 的 enum
- 和 TypeScript 的 enum / 联合类型做对比
- 帮你建立“Rust 思维”和“TS 思维”之间的映射
一、用一个例子打开 Rust 的 enum
rust
struct Product {
name: String,
category: ProductCategory,
price: f32,
in_stock: bool,
}
enum ProductCategory {
Books,
Clothing,
Electrics,
}可以对照 TypeScript 想象一下:
ts
enum ProductCategory {
Books,
Clothing,
Electrics,
}
type Product = {
name: string;
category: ProductCategory;
price: number;
inStock: boolean;
};这里两种语言的直觉是一致的:
- 都想把
category的取值限制在三个固定选项里 - 用类型系统防止你写出
"Book"、"book"这类拼写错误
再看 Command:
rust
enum Command {
Undo,
Redo,
AddText(String),
MoveCursor(i32, i32),
Replace { from: String, to: String },
}把它翻译成 TypeScript,更接近下面这种写法:
ts
type Command =
| { type: "Undo" }
| { type: "Redo" }
| { type: "AddText"; text: string }
| { type: "MoveCursor"; x: number; y: number }
| { type: "Replace"; from: string; to: string };在 Rust 里:
- 所有这些“形状不同”的命令,都被统一装进一个 enum
- 你可以用一个变量
command: Command表示“某种命令”
二、Rust 的 enum:不仅是“枚举值”,还是“类型的变体”
在很多语言里(C、Java 的传统 enum)枚举更像是一组常量:
RED = 0, GREEN = 1, BLUE = 2
而在 Rust 里,enum 更像是:
把多个“可能的形态”打包成一个统一的类型(代数数据类型里的“和类型” / Sum Type)
1️⃣ 最简单的“标签型”枚举
rust
enum ProductCategory {
Books,
Clothing,
Electrics,
}它只是三种可能的“标签”:
BooksClothingElectrics
这一层和 TypeScript 的 enum ProductCategory { ... } 很像。
2️⃣ 携带数据的枚举变体
Command 的强大之处在于:不同变体可以携带不同数据。
rust
enum Command {
Undo, // 不带数据
Redo, // 不带数据
AddText(String), // 携带一个 String
MoveCursor(i32, i32), // 携带两个 i32
Replace { from: String, to: String }, // 使用“字段名”的写法
}用 TS 思维来看:
- 每个变体就是一种“结构”
- 所有变体合起来,就是一个“有限集合的联合类型”
对应到 TypeScript:
ts
type Command =
| { type: "Undo" }
| { type: "Redo" }
| { type: "AddText"; text: string }
| { type: "MoveCursor"; x: number; y: number }
| { type: "Replace"; from: string; to: string };区别在于:
- Rust 用
enum+ 不同变体的“参数”来表达 - TypeScript 用“判别联合(discriminated union)”来表达
语法不一样,但抽象是一致的:
- TypeScript:用“对象联合 + 判别字段”来模拟 sum type
- Rust:内建了 sum type 语法,也就是 enum
三、模式匹配:Rust 使用 enum 的标准姿势
看 serialize2 方法:
rust
impl Command {
fn serialize2(&self) -> String {
let command_str = match self {
Command::Undo => String::from("{ \"cmd\": \"Undo\" }"),
Command::Redo => String::from("{ \"cmd\": \"Redo\" }"),
Command::AddText(text) => {
format!(
"{{
\"cmd\": \"AddText\",
\"text\": \"{text}\"
}}"
)
}
Command::MoveCursor(x, y) => {
format!(
"{{
\"cmd\": \"MoveCursor\",
\"x\": {x},
\"y\": {y}
}}"
)
}
Command::Replace { from, to } => {
format!(
"{{
\"cmd\": \"Replace\",
\"from\": \"{from}\",
\"to\": \"{to}\"
}}"
)
}
};
command_str
}
}关键点:
match self:对当前Command的具体变体做模式匹配- 每个分支写出对应的枚举变体
- 对于带数据的变体,直接解构出数据:
Command::AddText(text)Command::MoveCursor(x, y)Command::Replace { from, to }
把它翻译成 TypeScript 的写法,大致是:
ts
function serialize2(command: Command): string {
switch (command.type) {
case "Undo":
return '{ "cmd": "Undo" }';
case "Redo":
return '{ "cmd": "Redo" }';
case "AddText":
return `{
"cmd": "AddText",
"text": "${command.text}"
}`;
case "MoveCursor":
return `{
"cmd": "MoveCursor",
"x": ${command.x},
"y": ${command.y}
}`;
case "Replace":
return `{
"cmd": "Replace",
"from": "${command.from}",
"to": "${command.to}"
}`;
}
}Rust 的 match + enum 和 TypeScript 的 switch + 判别联合高度类似,但有两个关键加强点:
- 编译器会做穷尽性检查:漏掉任何一个变体都会报错
- 模式非常强大:可以嵌套解构、带
if条件(match+ifguard)
例如:
rust
fn handle(cmd: Command) {
match cmd {
Command::MoveCursor(x, y) if x == 0 && y == 0 => {
println!("光标已经在原点");
}
Command::MoveCursor(x, y) => {
println!("移动到 ({x}, {y})");
}
other => {
println!("其它命令:{}", other.serialize2());
}
}
}这里:
- 第一个分支只匹配“移动到 (0, 0)”这种特殊情况
- 第二个分支匹配其它
MoveCursor other把所有剩下的变体一网打尽
四、内存视角:enum 在底层长什么样?
理解内存布局有助于你建立更“底层”的心智模型。
可以简单地把 Rust 的 enum 想成:
一个“标签”(当前是哪个变体) + 一块能容纳“最大那个变体数据”的内存
举个比 size 的例子:
rust
enum Simple {
A,
B(u8),
C(u64),
}
fn main() {
println!("{}", std::mem::size_of::<Simple>());
}直觉:
C(u64)需要 8 字节- 再加上“标记当前是 A/B/C 的标签”所需的字节数
所以:
- enum 的大小 ≥ 最大变体大小 + 标签开销
- 不同变体共享同一块内存,但同一时刻只会有其中一种形态“处于激活状态”
和 TypeScript 对比:
- TS 的联合类型在运行时就是普通 JS 对象,没有专门的“enum 布局”概念
- Rust 的 enum 则是真正编译到机器码的数据结构,布局是编译器精确控制的
五、泛型 enum:Option / Result 才是 Rust 的日常
你在业务代码里最常遇到的 enum,往往不是自己写的 Command,而是标准库里的:
Option<T>Result<T, E>
1️⃣ Option:代替“值或 null”
定义大致是:
rust
enum Option<T> {
None,
Some(T),
}可以对比到 TypeScript:
ts
type Option<T> = T | undefined;在 Rust 里:
rust
fn find_user(id: u64) -> Option<String> {
if id == 1 {
Some("Alice".to_string())
} else {
None
}
}
fn demo() {
match find_user(1) {
Some(name) => println!("找到用户 {name}"),
None => println!("用户不存在"),
}
}区别于 TS:
- TypeScript 里你可以忘记判空,
user!.name等写法都能绕过去 - Rust 必须显式处理
Some/None,否则编译不过
2️⃣ Result:把异常变成类型的一部分
定义大致是:
rust
enum Result<T, E> {
Ok(T),
Err(E),
}你可以把它类比为:
ts
type Result<T, E> = { type: "Ok"; value: T } | { type: "Err"; error: E };示例:
rust
fn parse_num(s: &str) -> Result<i32, String> {
s.parse::<i32>().map_err(|e| e.to_string())
}
fn demo_result() {
match parse_num("10") {
Ok(n) => println!("数字是 {n}"),
Err(e) => println!("解析失败:{e}"),
}
}和 TypeScript 的对比:
- TS 常见写法是
try/catch或返回number | Error - Rust 把“成功 / 失败”编码进类型,用
Result<T, E>+match强制你处理错误
六、Rust enum vs TypeScript:相同点与关键差异
1️⃣ 相同点:都在表达“有限集合的可能形态”
无论是:
- Rust 的
enum Command { ... } - 还是 TypeScript 的
type Command = ... 联合
它们都在做同一件事:
把“有限个可能的形态”作为一个整体来建模
你拿到一个 command: Command,实际含义是:
- 这个值一定是那几种情况之一
- 编译器会要求你把所有情况都考虑到
2️⃣ Rust 的 enum 是“封闭”的
Rust 里的 enum 一旦定义:
- 变体集合就是固定的
- 不能在其他文件里“再扩展几种变体”
而 TypeScript 的联合类型:
- 可以通过交叉类型、再次声明等方式在别处扩展
封闭带来的好处是:
- Rust 的
match可以做 穷尽检查 - 漏掉任何一种变体,编译器都会报错
3️⃣ Rust enum 的每个变体是“强类型的”
在 Command 里:
AddText(String)明确只携带一个StringMoveCursor(i32, i32)明确是两个整数Replace { from: String, to: String }使用具名字段
这让你在 match 里解构时非常自然:
rust
match cmd {
Command::AddText(text) => { /* 使用 text */ }
Command::MoveCursor(x, y) => { /* 使用 x, y */ }
Command::Replace { from, to } => { /* 使用 from, to */ }
Command::Undo => { /* 不需要数据 */ }
Command::Redo => { /* 不需要数据 */ }
}TypeScript 的联合类型同样可以做到这一点,但类型系统是“结构化”的,Rust 则是“名义化”的 enum 体系。
4️⃣ TypeScript 的 enum 更多是“常量集合”
TypeScript 自带的 enum 在编译后会变成 JS 对象或数字:
ts
enum Direction {
Up,
Down,
}会被编译成类似:
js
var Direction;
(function (Direction) {
Direction[(Direction["Up"] = 0)] = "Up";
Direction[(Direction["Down"] = 1)] = "Down";
})(Direction || (Direction = {}));而真正承载“不同形态”的,是你的联合类型:
ts
type Message = { type: "Text"; text: string } | { type: "Image"; url: string };Rust 没有单独的“联合类型”语法,直接用 enum 同时完成“常量集合 + 不同形态数据”的工作。
七、和 Ownership / Borrowing 的微妙关系
五、和 Ownership / Borrowing 的微妙关系
枚举本身和 Ownership 规则完全一致:
Command::AddText(String)里的String也有所有权- 当这个枚举值被 move、被借用、被 drop 时,里面的数据也跟着走
例如:
rust
fn consume_command(cmd: Command) {
match cmd {
Command::AddText(text) => {
println!("{}", text);
}
_ => {}
}
}这里:
cmd的所有权被函数获取- 如果是
AddText变体,text的所有权被匹配分支接管 - 分支结束后,
text被释放
和结构体没有本质区别,只是“外壳是枚举”,内部照样遵守所有权和借用规则。
八、实战心智模型:从 TS 迁移到 Rust 时怎么想?
你可以记住这样一组映射关系:
| 需求 | TypeScript 写法 | Rust 写法 |
|---|---|---|
| 一组固定标签 | enum Color { Red, Green, Blue } | enum Color { Red, Green, Blue } |
| 每种情况有不同数据 | 判别联合 type Command = ... | enum Command { ... } |
| 根据不同情况执行不同逻辑 | switch (value.type) { ... } | match value { ... } |
| 强制处理所有可能情况 | 依赖 linter 或 never 检查 | 编译器穷尽检查 |
当你在 Rust 里看到一个 enum 时,可以直接用“TS 判别联合”的思维去理解它。
九、终极总结
- Rust 的 enum 不是“弱版的 C 枚举”,而是“强类型的代数数据类型”
- 简单场景下,它和 TypeScript 的
enum类似;复杂场景下,更像 TS 的判别联合 - 携带数据的变体,让一个 enum 可以优雅地表示多种不同形态
match+ enum 是 Rust 的核心组合,类似switch+ 联合类型,但更强- 配合 Ownership / Borrowing 思想,你可以用 enum 安全、清晰地表达各种业务状态
当你写代码时,只要能自然地把需求想成:
“它有几种互斥的形态,每种形态的数据结构不一样”
那么在 Rust 里,就毫不犹豫地定义一个 enum;
在 TypeScript 里,就定义一个判别联合。两种语言的思维,会在这里完美对齐。