Skip to content

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"
  • 操作 / 命令表达:编辑器命令、用户输入、游戏角色动作
    • 你的 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,
}

它只是三种可能的“标签”:

  • Books
  • Clothing
  • Electrics

这一层和 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 + if guard)

例如:

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) 明确只携带一个 String
  • MoveCursor(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 里,就定义一个判别联合。两种语言的思维,会在这里完美对齐。