Option:把十亿美元的错误编译掉
发明 null 的 Tony Hoare 后来管它叫「我的十亿美元错误」—— 因为它导致的崩溃、漏洞、损失,几十年来数以十亿计。Rust 的做法很干脆:没有 null。「可能没有值」这件事被写进类型 Option<T>,你想用里面的东西,就必须先处理「没有」的分支。空指针异常,就这样从一颗运行时炸弹,变成了一道编译期必答题。
问题出在哪:null 是个「披着 T 外衣」的陷阱
在有 null 的语言里,一个 String 类型的变量,实际上可能是两种东西:一个真的字符串,或者 null。可类型只写了 String,没有任何地方提醒你「它也可能是 null」。于是你一不留神就对着 null 调方法 —— NullPointerException,程序当场倒地。问题的根子是:类型撒了谎。它说这是个 String,其实这是个「String 或者虚无」,而「虚无」那一半被藏起来了,全靠你记得防。
Rust 的解法,是上一章那句话的直接应用 —— 不让类型撒谎。「可能没有」是一个真实的状态,那就把它明明白白写进类型:
enum Option<T> {
Some(T), // 有一个 T
None, // 没有
}
看清楚了 —— Option<T> 不是什么内置魔法,它就是上一章那种普通枚举。一个 Option<String> 诚实地告诉你:「我要么是个字符串,要么什么都没有,你自己看着办」。而一个 String(没包 Option)则保证它一定是个真字符串,永远不会是 null。类型不再撒谎,这是一切的起点。
关键:你绕不过「没有」那一半
光把「没有」写进类型还不够 —— 真正治本的是:你不能直接使用 Option<T> 里的 T,必须先「拆开」它,而拆开就必须处理 None。编译器不给你偷懒的路:
fn find_user(id: u32) -> Option<String> { /* ... */ }
let name = find_user(1);
// println!("{}", name.len()); // ✗ Option<String> 没有 .len(),你得先拆
match find_user(1) {
Some(name) => println!("找到了:{}", name), // 这里 name 保证是真字符串
None => println!("查无此人"), // 你被迫处理了「没有」
}
就是这一步「被迫处理 None」,让空指针异常无处发生。在别的语言里,「忘了检查 null」是默认行为,检查才是额外动作;在 Rust 里反过来 —— 「处理没有」是编译器强制的,忘不了。同一个问题,一个靠自觉,一个靠强制。
但这样不会很啰嗦吗:组合子来救场
你可能担心:难道每次可能为空,都要写一大坨 match?不会。Option 带了一大套组合子,让常见操作一行搞定,既安全又不啰嗦。挑最常用的:
| 你想要 | 写法 |
|---|---|
| 有值就变换它,没值就还是没值 | opt.map(|x| x.len()) |
| 没值就用个默认值 | opt.unwrap_or(0) / unwrap_or_else(|| ...) |
| 链式「有值再继续算,可能又为空」 | opt.and_then(|x| next(x)) |
| 只在有值时干点事 | if let Some(x) = opt { ... } |
| 把「没有」当错误往上抛 | opt.ok_or("没找到")?(下一章讲 ?) |
比如「找到用户就取名字长度,找不到就当 0」,别的语言要写 null 判断,Rust 一行:
let len = find_user(1).map(|name| name.len()).unwrap_or(0);
这行代码里,「找不到」的情况被 map(None 直接穿过)和 unwrap_or(给默认)悄悄处理好了 —— 安全性一分没少,啰嗦却没了。这套组合子风格,一开始不习惯,用顺了你会上瘾:它把「处理空」从一堆分散的 if 判断,收拢成一条清晰的数据流水线。
你会看到(也会想用)opt.unwrap() —— 它直接取出值,如果是 None 就 panic 崩溃。它是一条「我担保这里不可能为空」的捷径。问题是:新手容易到处 .unwrap() 图省事,等于把 null 那颗炸弹又亲手装了回来,只不过换了个引信。
规矩是:库代码、可能失败的地方,几乎永远不要 unwrap,老老实实返回 Option / Result 让调用者处理。unwrap / expect 只用在你能逻辑上确证不可能为空的地方(比如刚 insert 完立刻 get),而且优先用 expect("说清为什么这里不可能为空"),好让万一真崩了时,报错能告诉后人你当时的假设。
回流
Kotlin:最接近。Kotlin 的可空类型 String? 和 Rust 的 Option<String> 是同一个思想 —— 把「可能没有」编进类型,逼你用 ?.、?:、!! 去处理。区别是 Kotlin 用一个 ? 语法糖内建,Rust 用一个普通枚举 + 组合子。你写 Kotlin 时那句「这里可能是 null 吗」的警觉,原样搬到 Rust 就对了。而 Kotlin 的 !!,就是 Rust 的 .unwrap() —— 都是「我担保非空,错了就崩」的那颗自装炸弹。
Java:Optional<T> 是事后补的,问题是它没能消灭 null —— 一个 Optional 变量本身还能是 null,你还是可能对着它 NPE。它是「在满是 null 的世界里再加一层可选盒子」,而 Rust 是「从设计上就没有 null」。差别是根本性的:补丁 vs 根治。
Go:用 value, ok := m[key] 的双返回值,或者到处判 nil。能表达,但没有类型强制 —— 你完全可以忘了看那个 ok,编译器不拦。Rust 的 Option 是强制你拆的。
这一章的一句话
Rust 没有 null;「可能没有」被写进类型 Option<T>,你必须先处理「没有」才能碰到值 —— 空指针异常于是从运行时炸弹变成了编译期必答题。组合子让这份安全不啰嗦,unwrap 是你要慎用的自装炸弹。
「可能没有」解决了。还有一件同样常见的事:「可能失败」。下一章的 Result 是 Option 的孪生兄弟 —— 它把错误也变成一个普通的值,配上一个叫 ? 的小魔法。