阶段三 · 复合类型与控制流

枚举与 Option

一句话总结

Rust 的枚举(enum)远比 Go 的 iota 常量强大:每个变体可以携带不同类型的数据,是"代数数据类型".
最重要的应用是 Option--它彻底取代了 nil/null,把"可能没有值"编码进类型,让空指针错误在编译期消失.

枚举:不只是一组常量

最简单的枚举像 Go 的常量集合:

enum Direction {
North,
South,
East,
West,
}

let d = Direction::North;

但 Rust 枚举的真正威力在于:每个变体可以携带数据,而且各变体携带的数据类型可以不同.

enum Message {
Quit, // 无数据
Move { x: i32, y: i32 }, // 携带具名字段(像匿名结构体)
Write(String), // 携带一个 String
ChangeColor(i32, i32, i32), // 携带三个 i32
}

let msg = Message::Write(String::from("hello"));

这意味着一个 Message 值"要么是 Quit,要么是带坐标的 Move,要么是带字符串的 Write...",且类型系统强制处理每一种可能.

Go 里要表达"带数据的多态状态",通常得用 interface{} + 类型断言,既啰嗦又容易漏分支.
Rust 的枚举把所有可能性收拢进一个类型,编译器还会确保处理了每个变体--这是 Rust 类型系统最受称道的特性之一.

用枚举为"状态"建模

枚举特别适合表达"有限的几种状态/形态".比起用多个布尔标志或整数常量去拼,枚举让非法状态根本无法表示:

// 不好:用多个字段表达状态,可能出现矛盾组合
struct ConnBad {
is_connecting: bool,
is_connected: bool, // 两个都为 true 是非法的,但类型不阻止
error: Option<String>,
}

// 好:用枚举,每个状态互斥且各自携带恰当的数据
enum Connection {
Connecting,
Connected { session_id: u64 },
Failed(String),
}

最佳实践:让非法状态无法表示

"Make illegal states unrepresentable" 是 Rust 建模的核心思想.当几种状态互斥,且各自附带不同数据时,用枚举而非一堆布尔/可选字段.

这样编译器替排除了自相矛盾的组合.Go 缺少携带数据的枚举,这类约束只能靠运行时检查和约定,Rust 把它前移到了类型层面.

给枚举实现方法

和结构体一样,枚举也能有 impl 块和方法:

impl Message {
fn describe(&self) -> String {
match self {
Message::Quit => String::from("退出"),
Message::Move { x, y } => format!("移动到 ({x}, {y})"),
Message::Write(text) => format!("文本:{text}"),
Message::ChangeColor(r, g, b) => format!("颜色 {r},{g},{b}"),
}
}
}

Option:Rust 没有 null

枚举最重要的应用,是用标准库的 Option<T> 取代 nil:

enum Option<T> {
Some(T), // 有值,值是 T
None, // 无值
}

Rust 没有 null/nil.一个可能"没有值"的结果,类型就是 Option<T>,而编译器强制在取出值之前处理 None 的情况--无法像 Go 那样不小心对 nil 解引用:

fn find_even(nums: &[i32]) -> Option<i32> {
for &n in nums {
if n % 2 == 0 {
return Some(n);
}
}
None // 没找到
}

// 必须处理两种情况,否则拿不到里面的值
match find_even(&[1, 3, 4, 7]) {
Some(n) => println!("第一个偶数:{n}"),
None => println!("没有偶数"),
}

Go 里"未找到"常用返回 nil(value, ok),是否检查全靠自觉,漏检就 nil panic.
Rust 的 Option 把"可能没有"写进类型签名,不处理 None 就编译不过--空指针这个"十亿美元的错误"在 Rust 里被类型系统消灭了.

Option 的常用方法:别动不动就 match

每次都写完整 match 太啰嗦.Option 提供了大量便捷方法,熟练使用它们是写出地道 Rust 的标志:

方法 作用
unwrap_or(default) 有值取值,None 则用默认值
unwrap_or_else(|| ...) 同上,默认值由闭包惰性计算
map(|v| ...) 有值则变换,None 保持 None
and_then(|v| ...) 链式,闭包返回另一个 Option(扁平化)
is_some() / is_none() 判断是否有值
ok_or(err) 把 Option 转成 Result(第 14 篇用)
let config: Option<i32> = Some(5);

let port = config.unwrap_or(8080); // 有值取 5,否则用 8080
let doubled = config.map(|v| v * 2); // Some(10)
let len = Some("hi").map(|s| s.len()); // Some(2)

// 链式处理,优雅地避开层层 if 判空
let result = Some(4)
.map(|x| x + 1)
.filter(|&x| x > 3)
.unwrap_or(0);

最佳实践:用组合子代替手写判空

遇到 Option,先想 map/and_then/unwrap_or 这类"组合子(combinator)"能不能优雅表达,而不是一上来就 matchif let.这让代码更紧凑,意图更清晰.

当然,需要对两个分支都做复杂处理时,match 仍是最清楚的选择--下一篇专门讲它.

unwrap 是'我保证有值'的承诺,慎用

option.unwrap() 在 None 时会 panic.它适合原型,测试或能逻辑确证不为 None 的场景,但在生产业务路径上滥用 unwrap 等于放弃了 Option 的保护.

优先用 unwrap_or 等给出兜底,或用 ?(第 14 篇)向上传播.

承上启下

Option 和带数据的枚举都依赖一个强大工具来"拆开"--match 模式匹配.下一篇就专门讲它,以及如何优雅,穷尽地处理这些代数类型.

快速回顾

  • 枚举携带数据:每个变体可带不同类型的数据,是代数数据类型,远强于 Go 的 iota 常量.
  • 状态建模:用枚举让互斥状态各自携带恰当数据,使"非法状态无法表示".
  • 枚举有方法:同结构体一样可写 impl 块.
  • Option 取代 nil:Some(T)/None,编译器强制处理 None,消灭空指针错误.
  • 组合子优先:用 map/and_then/unwrap_or 等代替手写判空,更地道.
  • 慎用 unwrap:None 时 panic,生产路径优先用兜底或 ? 传播.

动手练习

  1. 枚举面积:定义一个 enum Shape,含 Circle(f64)Rectangle(f64, f64),并为它实现一个 area(&self) 方法.
  2. 状态建模:用枚举为"红绿灯"建模(红/黄/绿三态),思考它比用三个布尔字段好在哪.
  3. Option 处理:写一个返回 Option<f64> 的除法函数(除数为 0 返回 None),分别用 matchunwrap_or 处理结果.
  4. 链式组合子:给一个 Option<i32>map + filter + unwrap_or 链式处理--有值则平方,仅保留大于 10 的,否则得 0.
  5. 非法状态消除:把"用多个布尔字段表达连接状态"改写成一个枚举,体会"非法状态无法表示".