17-枚举

从 Go 的 tagged union 心智差异出发讲清 Rust 枚举、Option、Result 与变体数据

枚举 (Enums)

面向 Rust 1.97.1 (stable, 2026-07)。本篇假设你熟悉 Go,凡有可比性处均给出 🐹 Go 对比。

热度: hot 高频 | common 常见 | occasional 偶尔 | advanced 进阶少见

本篇能解决什么:

  • 你会不会把 Rust enum 想成 Go 的 iota 常量,结果完全低估它?
  • 你是否还不清楚为什么 Option/Result 是日常基础类型而不是“高级语法糖”?
  • 你会不会在 match 穷尽性、变体 move、递归 Box、#[repr]/FFI 上反复踩坑?
  • 你是否想知道:什么时候该用 enum,什么时候该拆成多个 struct / trait?

术语速查表:

术语 / 缩略词全称 / 读法中文一句话解释Go 里的近亲
enumenumeration枚举一个值属于有限几种变体之一,可变体带数据iota / 接口+类型断言近亲
variant—变体enum 的一种可能形状某具体实现类型
discriminant—判别式标记当前是哪个变体的标签手写 tag 字段
Option<T>—可选值Some(T) 或 None指针/ok 逗号布尔近亲
Result<T, E>—成功或失败Ok(T) 或 Err(E)(T, error)
exhaustiveness—穷尽性match 必须覆盖所有可能switch 通常不强制
Box<T>—堆分配智能指针把 T 放到堆上并拥有它近似 *T 但管释放
niche optimization—空位优化用非法位模式表示 None 等,省空间无直接对应
FFIForeign Function Interface外部函数接口与 C 等语言互操作cgo
ABIApplication Binary Interface应用二进制接口二进制层面的调用/布局约定同概念
#[non_exhaustive]—非穷尽标记告诉下游:以后还可能加变体/字段无直接对应
GCGarbage Collector垃圾回收器运行时回收Go 默认机制

说明:本表覆盖本篇出现的所有专业名词与缩略词;正文首次出现时仍会就地解释一次。

热度索引:

热度题目
hotQ1, Q2, Q3, Q4, Q18
commonQ5, Q6, Q7, Q8, Q9, Q10, Q11, Q17, Q19
occasionalQ12, Q13, Q14
advancedQ15, Q16

Q1. Rust 的 enum 为什么比 Go 的 iota 强得多?

Tags: hot beginner enum 适用版本: Rust 1.0+;#[non_exhaustive] stable

一句话答案:

Rust enum 是带标签的联合体:每个变体可以携带不同类型的数据,并且 match 能做穷尽检查;iota 只是一组整数常量。

解答:

Go 常用 const ( A = iota ...) 表达枚举常量,多形态数据往往靠接口或 struct { Kind; Payload }。Rust 把“哪一种 + 带什么数据”写进同一个类型,编译器因此知道所有可能形状。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
}

fn main() {
    let m = Message::Write(String::from("hi"));
    match m {
        Message::Quit => println!("quit"),
        Message::Move { x, y } => println!("{x},{y}"),
        Message::Write(s) => println!("{s}"),
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
enum Dir {
    N,
    S,
}

fn main() {
    let d = Dir::N;
    let name = match d {
        Dir::N => "north",
        Dir::S => "south",
    };
    println!("{name}");
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
enum Dir {
    N,
    S,
}

fn main() {
    let d = Dir::N;
    let _name = match d {
        Dir::N => "north",
    };
    // error[E0004]: non-exhaustive patterns: `Dir::S` not covered
}

Go 对比:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

const (
	DirN = iota
	DirS
)

func main() {
	d := DirN
	switch d {
	case DirN:
		fmt.Println("north")
	case DirS:
		fmt.Println("south")
	}
}
  • Go 怎么做:iota 给整数命名;复杂形态另用接口。
  • Rust 为什么不同:变体、数据、穷尽性进同一类型系统。
  • Go 程序员易踩的坑:把 enum 只当成常量列表。

Q2. 为什么 Option<T> 到处都是?

Tags: hot beginner enum 适用版本: Rust 1.0+

一句话答案:

Rust 没有 null;“可能没有值”用 Option<T>(Some(T) / None)在类型里显式表达,强迫你处理缺失情况。

解答:

Go 用 nil、零值或 (T, bool)。Rust 把可空写进类型:返回 Option<T> 时,调用方必须 match/if let/?(在返回 Option 的函数里)等。标准库很多查找 API(如 HashMap::get)都返回 Option。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
fn find_even(xs: &[i32]) -> Option<i32> {
    for &x in xs {
        if x % 2 == 0 {
            return Some(x);
        }
    }
    None
}

fn main() {
    match find_even(&[1, 3, 4]) {
        Some(v) => println!("{v}"),
        None => println!("none"),
    }
}
1
2
3
4
5
6
fn main() {
    let maybe = Some(String::from("hi"));
    if let Some(s) = maybe {
        println!("{s}");
    }
}
1
2
3
4
fn main() {
    let v = Some(1);
    println!("{}", v.unwrap_or(0));
}

Go 对比:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

func findEven(xs []int) (int, bool) {
	for _, x := range xs {
		if x%2 == 0 {
			return x, true
		}
	}
	return 0, false
}

func main() {
	if v, ok := findEven([]int{1, 3, 4}); ok {
		fmt.Println(v)
	}
}
  • Go 怎么做:nil 或 (value, ok)。
  • Rust 为什么不同:缺失是类型的一部分,不是约定。
  • Go 程序员易踩的坑:到处 unwrap(),把 Option 又用成“可能 panic 的 null”。

Q3. 为什么 Result<T, E> 是错误处理基础?

Tags: hot beginner enum 适用版本: Rust 1.0+

一句话答案:

可恢复错误用 Result<T, E>:Ok(T) 成功,Err(E) 失败;调用方必须处理或向上传递。

解答:

对应 Go 的 (T, error),但 Rust 用枚举把互斥结果编码进一个值,并用 ? 传播。panic! 留给真正的 bug,不替代日常错误。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
fn parse_even(s: &str) -> Result<i32, String> {
    let n: i32 = s
        .parse()
        .map_err(|e: std::num::ParseIntError| e.to_string())?;
    if n % 2 == 0 {
        Ok(n)
    } else {
        Err(String::from("not even"))
    }
}

fn main() {
    match parse_even("4") {
        Ok(n) => println!("{n}"),
        Err(e) => println!("err={e}"),
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
fn may_fail(ok: bool) -> Result<&'static str, &'static str> {
    if ok {
        Ok("yes")
    } else {
        Err("no")
    }
}

fn main() {
    let v = may_fail(true).unwrap_or("fallback");
    println!("{v}");
}
1
2
3
4
5
fn main() {
    let r: Result<i32, &str> = Err("boom");
    println!("{}", r.expect("must ok"));
    // 运行期 panic(不是编译错误):展示 expect 会在 Err 时崩
}

Go 对比:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
	"fmt"
	"strconv"
)

func parseEven(s string) (int, error) {
	n, err := strconv.Atoi(s)
	if err != nil {
		return 0, err
	}
	if n%2 != 0 {
		return 0, fmt.Errorf("not even")
	}
	return n, nil
}

func main() {
	if n, err := parseEven("4"); err != nil {
		fmt.Println(err)
	} else {
		fmt.Println(n)
	}
}
  • Go 怎么做:多返回值 (T, error)。
  • Rust 为什么不同:单个 Result 值 + 穷尽匹配/?。
  • Go 程序员易踩的坑:把 unwrap/expect 当默认错误处理。

Q4. 带数据的枚举变体到底怎么用?

Tags: hot beginner enum 适用版本: Rust 1.0+

一句话答案:

变体可以是无数据单元变体、元组变体或结构体变体;构造用 Enum::Variant(...),取出数据靠模式匹配。

解答:

像小型代数数据类型:不同分支携带不同载荷。匹配时把载荷绑到变量上;按值匹配非 Copy 载荷会 move,见 Q6。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
enum Msg {
    Quit,
    Write(String),
    Move { x: i32, y: i32 },
}

fn main() {
    let msgs = [
        Msg::Quit,
        Msg::Write(String::from("hi")),
        Msg::Move { x: 1, y: 2 },
    ];
    for m in &msgs {
        match m {
            Msg::Quit => println!("quit"),
            Msg::Write(s) => println!("write {s}"),
            Msg::Move { x, y } => println!("move {x},{y}"),
        }
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
enum Ip {
    V4(u8, u8, u8, u8),
    V6(String),
}

fn main() {
    let home = Ip::V4(127, 0, 0, 1);
    match home {
        Ip::V4(a, b, c, d) => println!("{a}.{b}.{c}.{d}"),
        Ip::V6(s) => println!("{s}"),
    }
}

Go 对比:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

type Msg struct {
	Kind string
	Text string
	X, Y int
}

func main() {
	m := Msg{Kind: "write", Text: "hi"}
	switch m.Kind {
	case "write":
		fmt.Println(m.Text)
	default:
		fmt.Println("other")
	}
}
  • Go 怎么做:常手写 kind + 可选字段。
  • Rust 为什么不同:每种变体只带自己需要的数据。
  • Go 程序员易踩的坑:做一个巨大 struct 让无关字段大量为零值。

Q5. 为什么 match 会强迫你处理所有变体?

Tags: common enum 适用版本: Rust 1.0+

一句话答案:

为了在新增变体或漏写分支时,编译期就失败,而不是运行到某条路径才崩。

解答:

漏分支会报 E0004。可用 _ => ... 显式吞掉剩余情况,但那会削弱“加变体必改代码”的保护;库的公开 enum 还可能标 #[non_exhaustive](见 Q14)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
enum Dir {
    N,
    E,
    S,
    W,
}

fn name(d: Dir) -> &'static str {
    match d {
        Dir::N => "north",
        Dir::E => "east",
        Dir::S => "south",
        Dir::W => "west",
    }
}

fn main() {
    println!("{}", name(Dir::E));
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
enum Dir {
    N,
    S,
}

fn main() {
    let d = Dir::N;
    let _ = match d {
        Dir::N => "north",
    };
    // error[E0004]: non-exhaustive patterns: `Dir::S` not covered
}

Go 对比:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

import "fmt"

func main() {
	d := 1
	switch d {
	case 1:
		fmt.Println("north")
		// 漏掉其他 case 也能编译
	}
}
  • Go 怎么做:switch 通常不强制穷尽。
  • Rust 为什么不同:把“漏处理”变成编译错误。
  • Go 程序员易踩的坑:习惯写部分 case,到 Rust 被 E0004 拦住。

Q6. 变体里的数据会不会被 move 出来?

Tags: common enum 适用版本: Rust 1.0+

一句话答案:

会。按值 match 非 Copy 载荷会把数据 move 出来,之后原 enum 值通常不能再用。

解答:

只想看一眼:匹配引用 match &m,或用 ref/ref mut(现代代码更常直接匹配引用)。需要拿走所有权再按值匹配。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
enum Msg {
    Write(String),
}

fn main() {
    let m = Msg::Write(String::from("hi"));
    match m {
        Msg::Write(s) => println!("{s}"),
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
enum Msg {
    Write(String),
}

fn main() {
    let m = Msg::Write(String::from("hi"));
    match m {
        Msg::Write(s) => println!("{s}"),
    }
    match m {
        Msg::Write(s) => println!("{s}"),
    }
    // error[E0382]: use of partially moved value: `m`
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
enum Msg {
    Write(String),
}

fn main() {
    let m = Msg::Write(String::from("hi"));
    match &m {
        Msg::Write(s) => println!("{s}"),
    }
    match &m {
        Msg::Write(s) => println!("again {s}"),
    }
}

Go 对比:

1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
	s := "hi"
	t := s
	fmt.Println(s, t)
}
  • Go 怎么做:字符串赋值复制头,原变量仍可用。
  • Rust 为什么不同:从变体取出 String 是所有权转移。
  • Go 程序员易踩的坑:match m 取出 String 后还想打印整个 m。

Q7. 什么时候该给枚举写方法?

Tags: common enum 适用版本: Rust 1.0+

一句话答案:

当行为属于“这个枚举值本身该会的事”(查询、转换、显示)时,在 impl Enum 里写方法,避免外部到处复制 match。

解答:

impl Message { fn describe(&self) -> ... } 写在枚举定义附近。内部仍用 match,但调用方只看到方法。跨许多无关类型的行为更适合 trait。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
enum Msg {
    Quit,
    Write(String),
}

impl Msg {
    fn describe(&self) -> String {
        match self {
            Msg::Quit => String::from("quit"),
            Msg::Write(s) => format!("write:{s}"),
        }
    }
}

fn main() {
    let m = Msg::Write(String::from("hi"));
    println!("{}", m.describe());
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
enum Traffic {
    Red,
    Green,
}

impl Traffic {
    fn can_go(&self) -> bool {
        matches!(self, Traffic::Green)
    }
}

fn main() {
    println!("{}", Traffic::Green.can_go());
}

Go 对比:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import "fmt"

type MsgKind int

const (
	Quit MsgKind = iota
	Write
)

func Describe(kind MsgKind, text string) string {
	switch kind {
	case Quit:
		return "quit"
	case Write:
		return "write:" + text
	default:
		return "?"
	}
}

func main() {
	fmt.Println(Describe(Write, "hi"))
}
  • Go 怎么做:常写包级函数 + switch。
  • Rust 为什么不同:方法挂在 enum 上,签名更清晰。
  • Go 程序员易踩的坑:把所有 match 散落在业务代码各处。

Q8. 判别式(discriminant)是什么?

Tags: common enum 适用版本: Rust 1.0+

一句话答案:

判别式是 enum 里标记“当前是哪个变体”的标签;有载荷时通常还有对应的数据区。

解答:

无载荷枚举的判别式有时可当整数用(as),但布局默认不保证稳定,跨语言别猜。需要稳定整数标签时用 #[repr(u8)] 等(见 Q9)。可用 std::mem::discriminant 比较“是不是同一变体”而不看载荷。

1
2
3
4
5
6
7
8
9
enum Color {
    Red = 1,
    Blue = 2,
}

fn main() {
    let c = Color::Blue;
    println!("{}", c as u8);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use std::mem::discriminant;

enum Msg {
    Quit,
    Write(String),
}

fn main() {
    let a = Msg::Quit;
    let b = Msg::Write(String::from("x"));
    println!("{}", discriminant(&a) == discriminant(&b));
}

Go 对比:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

import "fmt"

const (
	ColorRed  = 1
	ColorBlue = 2
)

func main() {
	fmt.Println(ColorBlue)
}
  • Go 怎么做:常量本身就是整数。
  • Rust 为什么不同:默认 enum 布局由编译器决定,不一定是你想的 C 式布局。
  • Go 程序员易踩的坑:把任意 enum as 成整数拿去持久化/FFI。

Q9. 什么时候要 #[repr(u8)]?

Tags: common enum 适用版本: Rust 1.0+

一句话答案:

需要和 C/协议约定固定整数宽度、或明确判别式整数类型时,把属性写在 enum 定义正上方:#[repr(u8)]。

解答:

位置:紧挨着 enum 上一行。常用于无载荷或 C-like 枚举。带复杂载荷的 enum 还要另看 #[repr(C)] 等,且 FFI(Foreign Function Interface,外部函数接口)必须与对方头文件一致,不能凭感觉。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#[repr(u8)]
enum Opcode {
    Nop = 0,
    Add = 1,
    Ret = 2,
}

fn main() {
    let op = Opcode::Add;
    println!("{}", op as u8);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#[repr(u8)]
enum Flag {
    Off = 0,
    On = 1,
}

fn from_u8(v: u8) -> Option<Flag> {
    match v {
        0 => Some(Flag::Off),
        1 => Some(Flag::On),
        _ => None,
    }
}

fn main() {
    println!("{}", from_u8(1).is_some());
}

Go 对比:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main

import "fmt"

type Opcode uint8

const (
	Nop Opcode = 0
	Add Opcode = 1
)

func main() {
	fmt.Println(uint8(Add))
}
  • Go 怎么做:自定义整数类型 + 常量。
  • Rust 为什么不同:默认 enum 表示不承诺固定宽度,要用 repr 钉死。
  • Go 程序员易踩的坑:没写 repr 就把 discriminant 当协议字节发到网上。

Q10. 为什么递归枚举常要 Box?

Tags: common enum 适用版本: Rust 1.0+

一句话答案:

编译器要知道类型的固定大小;若变体里直接嵌套同一个 enum,大小无限,必须用 Box 把递归部分放到堆上。

解答:

写法:enum List { Cons(i32, Box<List>), Nil }。Box<T> 是堆分配拥有指针,大小固定。Go 用指针天然递归;Rust 要显式间接层。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
enum List {
    Cons(i32, Box<List>),
    Nil,
}

fn main() {
    let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
    match list {
        List::Cons(v, _) => println!("{v}"),
        List::Nil => println!("nil"),
    }
}
1
2
3
4
5
enum Bad {
    Cons(i32, Bad),
    Nil,
}
// error[E0072]: recursive type `Bad` has infinite size

Go 对比:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

import "fmt"

type List struct {
	Val  int
	Next *List
}

func main() {
	l := &List{Val: 1, Next: &List{Val: 2}}
	fmt.Println(l.Val, l.Next.Val)
}
  • Go 怎么做:结构体里放 *List 即可递归。
  • Rust 为什么不同:值类型要有限大小,递归必须间接。
  • Go 程序员易踩的坑:写成 Cons(i32, List) 触发 E0072。

Q11. Option<&T> 为什么常常不额外占空间?

Tags: common enum 适用版本: Rust 1.0+

一句话答案:

引用不能是空;编译器做 niche optimization(空位优化):用“不可能出现的全零引用”表示 None,于是 Option<&T> 常与 &T 同宽。

解答:

这是布局优化,不是语言魔法特例 alone:Option<NonZeroU8> 等也有类似性质。不要依赖所有 Option<T> 都同大小;对具体类型用 size_of 验证。

1
2
3
4
5
6
use std::mem::size_of;

fn main() {
    assert_eq!(size_of::<&i32>(), size_of::<Option<&i32>>());
    println!("{}", size_of::<Option<&i32>>());
}
1
2
3
4
5
6
fn main() {
    let x = 1;
    let a: Option<&i32> = Some(&x);
    let b: Option<&i32> = None;
    println!("{} {}", a.is_some(), b.is_none());
}

Go 对比:

1
2
3
4
5
6
7
8
package main

import "fmt"

func main() {
	var p *int
	fmt.Println(p == nil)
}
  • Go 怎么做:指针本身可以为 nil。
  • Rust 为什么不同:引用非空,缺失交给 Option,再靠空位优化省空间。
  • Go 程序员易踩的坑:以为所有 Option 都比 T 多一个字节标签。

Q12. Go 里常用接口 + struct 模拟多形态,Rust enum 有什么优势?

Tags: occasional enum 适用版本: Rust 1.0+

一句话答案:

变体集合封闭且已知时,enum + match 更快、更易穷尽;开放扩展、多类型实现同一行为时才更像 Go 接口那样用 trait。

解答:

协议消息、状态机、AST 节点这类“就这几种”用 enum。插件式后端、第三方类型要实现同一能力用 trait 对象或泛型。两者可组合:enum 里再装 Box<dyn Trait>。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
enum Shape {
    Circle(f64),
    Rect(f64, f64),
}

fn area(s: &Shape) -> f64 {
    match s {
        Shape::Circle(r) => std::f64::consts::PI * r * r,
        Shape::Rect(w, h) => w * h,
    }
}

fn main() {
    println!("{}", area(&Shape::Rect(3.0, 4.0)));
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
trait Area {
    fn area(&self) -> f64;
}

struct Circle(f64);
impl Area for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.0 * self.0
    }
}

fn main() {
    let c = Circle(2.0);
    println!("{}", c.area());
}

Go 对比:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main

import "fmt"

type Shape interface{ Area() float64 }

type Rect struct{ W, H float64 }

func (r Rect) Area() float64 { return r.W * r.H }

func main() {
	var s Shape = Rect{W: 3, H: 4}
	fmt.Println(s.Area())
}
  • Go 怎么做:接口是默认多形态工具。
  • Rust 为什么不同:封闭集合用 enum 更直接、可穷尽。
  • Go 程序员易踩的坑:凡事先上 dyn Trait,丢掉穷尽匹配好处。

Q13. 什么时候该拆成多个 struct,而不是一个大 enum?

Tags: occasional enum 适用版本: Rust 1.0+

一句话答案:

变体几乎无共享逻辑、或某些状态有大量独有字段/长生命周期流程时,拆成独立类型(再在边界用 enum/trait 汇总)。

解答:

信号:某个变体的方法与其它变体毫无关系;或 match 分支里出现巨大独立模块。相反,状态转换频繁且要统一处理时,留在一个 enum 更清晰。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct Draft {
    body: String,
}
struct Published {
    body: String,
    views: u64,
}

enum Post {
    Draft(Draft),
    Published(Published),
}

fn main() {
    let p = Post::Draft(Draft {
        body: String::from("hi"),
    });
    match p {
        Post::Draft(d) => println!("draft {}", d.body),
        Post::Published(p) => println!("{} {}", p.body, p.views),
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
enum TooBig {
    A { a1: i32, a2: i32, a3: i32 },
    B { b1: String, b2: String, b3: String, b4: String },
}

fn main() {
    let x = TooBig::A { a1: 1, a2: 2, a3: 3 };
    match x {
        TooBig::A { a1, .. } => println!("{a1}"),
        TooBig::B { b1, .. } => println!("{b1}"),
    }
}

Go 对比:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main

import "fmt"

type Draft struct{ Body string }
type Published struct {
	Body  string
	Views uint64
}

func main() {
	d := Draft{Body: "hi"}
	fmt.Println(d.Body)
}
  • Go 怎么做:不同状态常是不同 struct + 接口。
  • Rust 为什么不同:可用 enum 统一,也可在变大后拆分。
  • Go 程序员易踩的坑:一个 enum 塞进整个业务域的所有状态。

Q14. #[non_exhaustive] 什么时候值得用?

Tags: occasional enum 适用版本: Rust 1.0+;#[non_exhaustive] stable

一句话答案:

写公共库、未来可能加变体(或结构体字段)又不想让下游 match 在小版本升级时突然非穷尽失败时,把属性标在类型定义上方。

解答:

写法:在 pub enum / pub struct 上一行写 #[non_exhaustive]。效果:外部 crate 必须写 _ 分支(enum)或不能用全字段字面量构造(struct)。本 crate 内部仍可穷尽匹配。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#[non_exhaustive]
pub enum ErrorKind {
    NotFound,
    PermissionDenied,
}

fn main() {
    let e = ErrorKind::NotFound;
    match e {
        ErrorKind::NotFound => println!("404"),
        ErrorKind::PermissionDenied => println!("403"),
        // 同 crate 内仍可穷尽;跨 crate 时需要 `_ => ...`
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#[non_exhaustive]
pub struct Config {
    pub host: String,
}

fn main() {
    let c = Config {
        host: String::from("localhost"),
    };
    println!("{}", c.host);
}

Go 对比:

1
2
3
4
5
6
7
8
package main

import "fmt"

// Go 无同名属性;库演进靠文档约定与新接口
func main() {
	fmt.Println("no non_exhaustive")
}
  • Go 怎么做:靠版本与文档约束客户端。
  • Rust 为什么不同:可用属性强制下游保留通配分支。
  • Go 程序员易踩的坑:库 enum 不加该属性,加变体变成破坏性变更。

Q15. FFI 场景下为什么不能乱猜 enum 布局?

Tags: advanced enum 适用版本: Rust 1.0+

一句话答案:

默认 Rust enum 的 ABI(Application Binary Interface,应用二进制接口)布局不保证与 C 一致;跨语言必须显式 repr 并与对方定义对齐。

解答:

与 C 互操作时:无载荷常用 #[repr(u8)]/#[repr(C)] 的 C-like enum;带数据的联合体要按 FFI 指南设计,而不是把日常 Rust enum 直接 transmute。不确定就在双方用整数标签 + 独立 payload。

1
2
3
4
5
6
7
8
9
#[repr(C)]
enum CLike {
    A = 1,
    B = 2,
}

fn main() {
    println!("{}", CLike::B as i32);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#[repr(u8)]
enum Wire {
    Ping = 1,
    Pong = 2,
}

fn main() {
    let tag = Wire::Ping as u8;
    println!("{tag}");
}

Go 对比:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main

import "fmt"

type Wire uint8

const (
	Ping Wire = 1
	Pong Wire = 2
)

func main() {
	fmt.Println(uint8(Ping))
}
  • Go 怎么做:整数常量直接当协议标签。
  • Rust 为什么不同:enum 默认是 Rust 自己的表示,不是 C enum。
  • Go 程序员易踩的坑:把复杂 Rust enum 指针丢给 C 却不设 repr。

Q16. 本章最重要的枚举设计建议是什么?

Tags: advanced enum 适用版本: Rust 1.0+

一句话答案:

封闭且已知的形状用 enum;可空/可失败用 Option/Result;匹配优先借用不乱 move;递归加 Box;对外稳定 API 考虑 non_exhaustive/repr。

解答:

检查清单:

  1. 别用 iota 心智建模——先问变体要不要带数据。
  2. 缺省值/错误不要用哨兵整数,用 Option/Result。
  3. match 按引用,除非你真要拿走载荷。
  4. 开放扩展用 trait;封闭协议用 enum。
  5. FFI/协议整数标签才上 repr。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
enum Cmd {
    Quit,
    Echo(String),
}

impl Cmd {
    fn run(&self) {
        match self {
            Cmd::Quit => println!("bye"),
            Cmd::Echo(s) => println!("{s}"),
        }
    }
}

fn main() {
    Cmd::Echo(String::from("hi")).run();
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn maybe_id(ok: bool) -> Option<u32> {
    if ok {
        Some(7)
    } else {
        None
    }
}

fn main() {
    println!("{:?}", maybe_id(true));
}

Go 对比:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import "fmt"

func maybeID(ok bool) (uint32, bool) {
	if ok {
		return 7, true
	}
	return 0, false
}

func main() {
	if id, ok := maybeID(true); ok {
		fmt.Println(id)
	}
}
  • Go 怎么做:常量 + 接口 + (T, error)/(T, bool)。
  • Rust 为什么不同:enum 把形状与穷尽性变成默认工具。
  • Go 程序员易踩的坑:继续用魔法数字和可空指针表达状态。

Q17. 为什么一个大变体会撑大整个 enum?

Tags: common enum layout size 适用版本: Rust 1.0+

一句话答案:

enum 的大小至少要装得下最大那个变体(外加判别式等),所以一个「胖」变体会抬高所有变体所占空间;大载荷请 Box 起来,或拆成多个类型。

解答:

布局直觉:同一时刻只存一个变体,但静态大小按最坏情况预留:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::mem::size_of;

enum Slim {
    A(u8),
    B(u16),
}

enum Fat {
    Small(u8),
    Huge([u8; 1024]),
}

enum Boxed {
    Small(u8),
    Huge(Box<[u8; 1024]>), // 变体里只多一个指针级大小
}

fn main() {
    println!("Slim={}", size_of::<Slim>());
    println!("Fat={}", size_of::<Fat>()); // 被 Huge 抬高
    println!("boxed={}", size_of::<Boxed>());
}

何时 Box:某个变体偶尔出现却极大,或你大量存放该 enum(Vec<Enum>)时。何时拆类型:变体集合并不总一起出现,用独立 struct + trait/Result 更清晰(见 Q13)。

1
2
// 心智:size_of::<Enum>() ≈ discriminant + max(size_of 各变体载荷)(还有对齐)
// 一个 [u8; 1_000_000] 变体 → 整个 enum 都约莫那么「胖」

Go 对比:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package main

import "fmt"

type Msg interface{} // 接口值:类型信息 + 数据指针,不按「最大变体」预留连续块

func main() {
	var m Msg = [1024]byte{}
	fmt.Printf("%T\n", m)
}
  • Go 怎么做:接口/指针间接,大对象常在堆上,容器存的是小头。
  • Rust 为什么不同:enum 默认内联最大变体,换来匹配快、少一层间接。
  • Go 程序员易踩的坑:按「接口一样大」估 Vec<Enum> 内存,结果被一个胖变体拖垮。

记忆点:

  • enum 大小 ≈ 标签 + 最大变体。
  • 偶发巨载荷 → Box 该变体。
  • 形状不闭合 → 考虑拆类型,而不是一个超级 enum。

Q18. Option 的 as_ref / as_mut / as_deref 怎么用?

Tags: hot option as_ref as_deref 适用版本: Rust 1.0+;as_deref / as_deref_mut 需 1.40+

一句话答案:

要看清里面的值又不搬走:as_ref → Option<&T>,as_mut → Option<&mut T>;里面是 String/Vec/Box 等智能指针、想直接拿到 &str/&[T]/&T 时用 as_deref(或 as_deref_mut)。

解答:

按值 match/unwrap 会 move 出 T。先变成「可选的引用」更常见:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
fn show(name: &Option<String>) {
    match name.as_ref() {
        Some(s) => println!("{s}"),
        None => println!("(none)"),
    }
}

fn main() {
    let mut name = Some(String::from("Ada"));
    show(&name);
    if let Some(s) = name.as_mut() {
        s.push('!');
    }
    show(&name);
}

as_deref:在 as_ref 之后再按 Deref 剥一层,适合 Option<String> → Option<&str>:

1
2
3
4
5
6
7
8
9
fn len(name: &Option<String>) -> usize {
    name.as_deref().map_or(0, str::len)
}

fn main() {
    let name = Some(String::from("Ada"));
    println!("{}", len(&name));
    // 等价直觉:name.as_ref().map(|s| s.as_str())
}

选型:只读借用内容 → as_ref;要改内容 → as_mut;要 &str/&[T] 这种已解引用视图 → as_deref。不要用 clone 灭火替代这三者。

Go 对比:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

import "fmt"

func main() {
	var p *string
	s := "Ada"
	p = &s
	if p != nil {
		fmt.Println(*p) // 指针可空;没有 as_ref 这套转换
	}
}
  • Go 怎么做:*T / 逗号 ok 表达可空,解引用靠 *。
  • Rust 为什么不同:Option<T> 与引用组合要显式转换,避免误 move。
  • Go 程序员易踩的坑:对 Option<String> 直接 *o 式思维,或疯狂 clone。

记忆点:

  • as_ref / as_mut:可选引用,防 move。
  • as_deref:再 Deref 一层(如 String → str)。
  • 能借就不 clone。

Q19. 怎么用 FromStr / TryFrom 把字符串转成 enum?

Tags: common enum FromStr TryFrom parse 适用版本: Rust 1.0+

一句话答案:

解析文本用 FromStr(配合 "red".parse::<Color>()?);从已有类型(u8、别的 enum、&str 以外的输入)fallible 转换用 TryFrom——两者都返回 Result,比魔法 unwrap 映射表更合适。

解答:

FromStr 接 parse:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
use std::str::FromStr;

#[derive(Debug, PartialEq)]
enum Color {
    Red,
    Green,
}

#[derive(Debug)]
struct ParseColorError;

impl FromStr for Color {
    type Err = ParseColorError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "red" => Ok(Color::Red),
            "green" => Ok(Color::Green),
            _ => Err(ParseColorError),
        }
    }
}

fn main() {
    let c: Color = "red".parse().unwrap();
    assert_eq!(c, Color::Red);
    assert!("blue".parse::<Color>().is_err());
}

TryFrom 适合「从别的类型试着转」:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::convert::TryFrom;

#[derive(Debug, PartialEq)]
enum Op {
    Add,
    Sub,
}

impl TryFrom<u8> for Op {
    type Error = ();

    fn try_from(v: u8) -> Result<Self, Self::Error> {
        match v {
            1 => Ok(Op::Add),
            2 => Ok(Op::Sub),
            _ => Err(()),
        }
    }
}

fn main() {
    assert_eq!(Op::try_from(1).unwrap(), Op::Add);
    assert!(Op::try_from(9).is_err());
}

From/Into 只用于不会失败的转换;会失败就留下 TryFrom/FromStr。错误类型在库代码里应比 () 更有信息量。

Go 对比:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
	"errors"
	"fmt"
)

type Color int

const (
	ColorRed Color = iota
	ColorGreen
)

func ParseColor(s string) (Color, error) {
	switch s {
	case "red":
		return ColorRed, nil
	case "green":
		return ColorGreen, nil
	default:
		return 0, errors.New("bad color")
	}
}

func main() {
	c, err := ParseColor("red")
	fmt.Println(c, err)
}
  • Go 怎么做:手写 ParseX(string) (T, error)。
  • Rust 为什么不同:标准 trait 让 .parse() / TryFrom 成为惯例,泛型边界也好写。
  • Go 程序员易踩的坑:用 iota 整数乱转 enum,却不校验非法值。

记忆点:

  • 文本 → enum:FromStr + .parse()。
  • 其他类型 → enum:TryFrom。
  • 可能失败 → 不要用 From 硬转。

最后修改 August 11, 2026: 更新 (70a5af133)