用类型表达:没有 null,也没有异常

Option:没有 null 怎么活

它在解决什么

Tony Hoare 把 null 叫做「价值十亿美元的错误」,因为他 1965 年在 ALGOL W 里 引入了它。那个错误的具体形状是:一个类型说自己是 String, 而运行时它可能什么都不是,于是每一次使用都是一次赌博。

Rust 没有 null。它用一个普通的 enum 顶替了这件事 —— 普通这个词是本篇的重点。

它真的没有魔法

Option<T> 在标准库里的定义就是两行。你完全可以自己写一个等价的:

enum Maybe<T> {
    Just(T),
    Nothing,
}

跑起来和 Option 一模一样,包括漏一个分支时那个 E0004 —— 因为它就是个 enum,模式匹配那篇讲的一切原样适用。

⇒ 这件事值得说清楚,因为从有 null 的语言过来的人容易把 Option 当成「带语法糖的可空类型」。它不是。它是一个值,装在一个盒子里, 而你想用里面的东西就得先把盒子打开。

类型逼着你打开那个盒子

这一行编译不过:

let name: Option<String> = None;
println!("{}", name.to_uppercase());   // E0599

Option<String> 不是 String,它上面没有 to_uppercase。 在有 null 的语言里,同样的代码编译通过、运行时抛 NPE; 这里它连编译都过不去。

⇒ 这就是「没有 null」的全部机制。不是运行时多了什么检查, 而是**「可能没有」这件事被写进了类型**,于是编译器能在类型对不上的时候拦下来。

⚠️ 自己试的时候别用 .len()。它报的是另一回事 —— E0624: method \len` is private,因为 Option内部真的有一个**私有的**len`。 这句话和「没有 null」毫无关系,只会让人以为是可见性问题。

打开盒子的几种方式

从最粗暴到最讲究:

写法 什么时候用
unwrap() 你能证明它不可能是 None。失败就 panic
expect("理由") 同上,但把理由写进去
unwrap_or(默认值) 有就用,没有就用默认值
unwrap_or_default() 同上,默认值取类型的 Default
map / and_then 不打开,直接在里面做变换
match / if let 两种情况要做的事不一样
? 没有就让整个函数提前返回 None

unwrap 不是坏东西,但它的错误信息是

unwrap 在 None 上会 panic, 展开那条示例的「运行时说了什么」能看到它说的原话:

called `Option::unwrap()` on a `None` value

这句话什么都没告诉你 —— 不知道是哪个业务假设错了,也不知道该去看什么。

⇒ 判据:unwrap 本质上是一句断言,那就把断言的理由写出来 —— 用 expect("配置里 name 必填,加载时已校验")。出事的时候, 那句话就是现成的诊断。

map 包一层,and_then 展平

这两个最容易混,但判据只有一句: 闭包返回的是不是又一个 Option。

s.split(' ').next().map(|w| w.len())            // w.len() 是 usize     → map
s.split(' ').next().and_then(|w| w.parse().ok()) // .ok() 是 Option<i32> → and_then

用错就是 E0308:类型变成了 Option<Option<i32>>。

📌 如果你写过 JS/Java 的 flatMap,and_then 就是它。

?:没有就提前走人

在返回 Option 的函数里,? 的意思是 「是 Some 就取出来继续,是 None 就直接 return None」:

fn initials(full: &str) -> Option<String> {
    let mut parts = full.split(' ');
    let first = parts.next()?.chars().next()?;
    let last = parts.next()?.chars().next()?;
    Some(format!("{first}.{last}."))
}

⭐ 对照项把四个 ? 换成 .unwrap(), 第一行照常打印出来了,然后在第二次调用时 panic。

这正是 unwrap 最难查的地方:它不在你写的时候出事, 在某个特定输入进来的时候出事。而 ? 把同一件事变成了「返回 None」—— 调用方拿到的是一个值,不是一次崩溃。

🚨 match name 会把它吃掉

这是真实代码里撞得最多的一个,而且它和 Option 本身无关,是所有权在这里的体现。

加不加那个 &,差别是整个变量还在不在:

match &name { Some(s) => ..., None => ... }   // name 一点没动
match name  { Some(s) => ..., None => ... }   // name 被移动进去了,之后不能再用

原因很直白:Option<String> 里装的 String 不是 Copy, 所以 match name 是一次移动。 之后再用 name 就是 E0382。

加上 & 之后,s 会自动变成 &String(这叫默认绑定模式, 编译器帮你把模式里的绑定也变成引用),原来那个 name 完好无损。

📌 方法版是同一件事:name.as_ref() 给你 Option<&String>, name.as_deref() 给你 Option<&str>。看到别人写 as_ref() 时, 他多半就是在躲这个坑。

下一步

Option 回答的是「可能没有」。而「可能失败」是另一件事 —— 失败通常还要带上为什么失败。见 《Result 与 ?:没有异常怎么活》: Result 就是加了这一维的 Option,而 ? 在它身上还要更常用。

全部篇目见 Rust 教程首页。

本篇示例

下面每一条都是完整的、能单独编译的程序,由npm run test:rust 在每次构建前用真的 rustc 跑一遍。 「编译不过、报 E0382」这种话在这里是被验证过的断言。 报错原文和对照项的结果由同一道闸门自动回写,会随工具链更新,但不作为断言。

`Option` 没有魔法 —— 自己写一个一模一样的

enum Maybe<T> {
    Just(T),
    Nothing,
}

fn first_char(s: &str) -> Maybe<char> {
    match s.chars().next() {
        Some(c) => Maybe::Just(c),
        None => Maybe::Nothing,
    }
}

fn show(m: Maybe<char>) {
    match m {
        Maybe::Just(c) => println!("有:{c}"),
        Maybe::Nothing => println!("没有"),
    }
}

fn main() {
    show(first_char("hi"));
    show(first_char(""));
}

编译通过 · 输出 "有:h\n没有\n"

对照:删掉 `Maybe::Nothing` 那个分支
enum Maybe<T> {
    Just(T),
    Nothing,
}

fn first_char(s: &str) -> Maybe<char> {
    match s.chars().next() {
        Some(c) => Maybe::Just(c),
        None => Maybe::Nothing,
    }
}

fn show(m: Maybe<char>) {
    match m {
        Maybe::Just(c) => println!("有:{c}"),
    }
}

fn main() {
    show(first_char("hi"));
    show(first_char(""));
}

编译不过:error[E0004]

`Option<T>` 在标准库里的定义就是这么两行。它不是语言内置的特殊语法,只是一个带两个变体的泛型 enum —— 所以[模式匹配那篇](/rust/types/pattern-matching/)讲的一切对它原样适用,包括对照项那个 `E0004`。

类型逼着你处理「没有」

fn main() {
    let name: Option<String> = None;
    println!("{}", name.to_uppercase());
}

编译不过 · error[E0599]

rustc 原文
error[E0599]: no method named `to_uppercase` found for enum `Option<T>` in the current scope
 --> option-cannot-be-used-directly.rs:3:25
  |
3 |     println!("{}", name.to_uppercase());
  |                         ^^^^^^^^^^^^ method not found in `Option<String>`
  |
note: the method `to_uppercase` exists on the type `String`
 --> /rustc/48a229ceaefd4985c50990b14116b6d856af0985/library/alloc/src/str.rs:640:4
help: consider using `Option::expect` to unwrap the `String` value, panicking if the value is an `Option::None`
  |
3 |     println!("{}", name.expect("REASON").to_uppercase());
  |                        +++++++++++++++++
对照:改成 `name.map(|s| s.to_uppercase()).unwrap_or_default()`
fn main() {
    let name: Option<String> = None;
    println!("{}", name.map(|s| s.to_uppercase()).unwrap_or_default());
}

编译通过,输出:"\n"

这就是「没有 null」的全部含义:`Option<String>` **不是** `String`,你不能假装它是。在有 null 的语言里这一行编译通过、运行时炸;这里它连编译都过不去(`E0599`:`Option<T>` 上没有 `to_uppercase`)。⚠️ 自己试的时候别用 `.len()` —— `Option` 内部有一个**私有的** `len`,于是报出来的是 `E0624: method `len` is private`,一个和「没有 null」毫无关系的误导性错误。(这条是写这篇时实测撞出来的。)

🚨 别用 `.len()` 试 —— 你会撞到一个误导性的报错

fn main() {
    let name: Option<String> = None;
    println!("{}", name.len());
}

编译不过 · error[E0624]

rustc 原文
error[E0624]: method `len` is private
 --> option-private-len-trap.rs:3:25
  |
3 |     println!("{}", name.len());
  |                         ^^^ private method
  |
 --> /rustc/48a229ceaefd4985c50990b14116b6d856af0985/library/core/src/option.rs:808:4
  |
  = note: private method defined here
note: the method `len` exists on the type `String`
 --> /rustc/48a229ceaefd4985c50990b14116b6d856af0985/library/alloc/src/string.rs:1865:4
help: consider using `Option::expect` to unwrap the `String` value, panicking if the value is an `Option::None`
  |
3 |     println!("{}", name.expect("REASON").len());
  |                        +++++++++++++++++
对照:换成 `Option` 上确实没有的方法 `to_uppercase()`
fn main() {
    let name: Option<String> = None;
    println!("{}", name.to_uppercase());
}

编译不过:error[E0599]

想演示「`Option<String>` 不是 `String`」时,`.len()` 是最顺手的例子 —— 而它报的是 `E0624: method \`len\` is private`,因为 **`Option` 内部真的有一个私有的 `len`**。这句话和「没有 null」毫无关系,只会让人以为是可见性问题。对照项换成 `to_uppercase()` 才给出想要的 `E0599: no method named ...`。⇒ 这条示例存在的理由不是教语法,是**把一个会带偏读者的报错先摆出来**。

`unwrap` 是「我保证它有」—— 保证错了就 panic

fn main() {
    let v: Vec<i32> = Vec::new();
    let first = v.first().unwrap();
    println!("{first}");
}

编译通过,但运行时 panic · 退出码 101

运行时说了什么

thread 'main' panicked at option-unwrap-panics.rs:3:27:
called `Option::unwrap()` on a `None` value
对照:改成 `unwrap_or(&0)`
fn main() {
    let v: Vec<i32> = Vec::new();
    let first = v.first().unwrap_or(&0);
    println!("{first}");
}

编译通过,输出:"0\n"

⚠️ `unwrap` 不是坏东西,它是一句**断言**:「这里不可能是 None」。问题在于它的失败信息什么都没说(对照展开「运行时说了什么」看那句 `called \`Option::unwrap()\` on a \`None\` value`)。⇒ 真要断言就用 `expect("为什么它不该为空")`,把理由写进去 —— 出事时那句话就是现成的诊断。

`map` 包一层,`and_then` 展平

fn first_word_len(s: &str) -> Option<usize> {
    s.split(' ').next().map(|w| w.len())
}

fn parse_first(s: &str) -> Option<i32> {
    s.split(' ').next().and_then(|w| w.parse().ok())
}

fn main() {
    println!("{:?} {:?}", first_word_len("hello world"), parse_first("42 x"));
    println!("{:?}", parse_first("no 1"));
}

编译通过 · 输出 "Some(5) Some(42)\nNone\n"

对照:把 `and_then` 换成 `map`
fn first_word_len(s: &str) -> Option<usize> {
    s.split(' ').next().map(|w| w.len())
}

fn parse_first(s: &str) -> Option<i32> {
    s.split(' ').next().map(|w| w.parse().ok())
}

fn main() {
    println!("{:?} {:?}", first_word_len("hello world"), parse_first("42 x"));
    println!("{:?}", parse_first("no 1"));
}

编译不过:error[E0308]

判据只有一句:**闭包返回的是不是又一个 `Option`**。返回普通值用 `map`(`w.len()` 是 `usize`),返回 `Option` 用 `and_then`(`w.parse().ok()` 是 `Option<i32>`)。用错就是对照项那个 `E0308` —— 类型变成了 `Option<Option<i32>>`。

🚨 `match name` 会把它吃掉,`match &name` 不会

fn main() {
    let name: Option<String> = Some(String::from("ann"));
    match &name {
        Some(s) => println!("有 {s}"),
        None => println!("没有"),
    }
    println!("{name:?}");
}

编译通过 · 输出 "有 ann\nSome(\"ann\")\n"

对照:把 `match &name` 的 `&` 去掉
fn main() {
    let name: Option<String> = Some(String::from("ann"));
    match name {
        Some(s) => println!("有 {s}"),
        None => println!("没有"),
    }
    println!("{name:?}");
}

编译不过:error[E0382]

这是真实代码里撞得最多的一个:`Option<String>` 里装的 `String` 不是 `Copy`,所以 `match name` 会把 `name` **移动**进 match,之后就不能再用了(对照项 `E0382`)。加个 `&` 之后,`s` 自动变成 `&String`(这叫默认绑定模式),原来那个 `name` 一点没动。⇒ 顺手记一条:`as_ref()` / `as_deref()` 是同一件事的方法版。

`?` 在返回 `Option` 的函数里:没有就提前走人

fn initials(full: &str) -> Option<String> {
    let mut parts = full.split(' ');
    let first = parts.next()?.chars().next()?;
    let last = parts.next()?.chars().next()?;
    Some(format!("{first}.{last}."))
}

fn main() {
    println!("{:?}", initials("ann bob"));
    println!("{:?}", initials("ann"));
}

编译通过 · 输出 "Some(\"a.b.\")\nNone\n"

对照:把四个 `?` 都换成 `.unwrap()`
fn initials(full: &str) -> Option<String> {
    let mut parts = full.split(' ');
    let first = parts.next().unwrap().chars().next().unwrap();
    let last = parts.next().unwrap().chars().next().unwrap();
    Some(format!("{first}.{last}."))
}

fn main() {
    println!("{:?}", initials("ann bob"));
    println!("{:?}", initials("ann"));
}

编译通过,但运行时退出码 101

⭐ 对照项**第一行照常打印出来了**,然后在第二次调用时 panic —— 这正是 `unwrap` 最难查的地方:它不在写的时候出事,在某个特定输入进来的时候出事。`?` 把同一件事变成「返回 `None`」,调用方拿到的是一个值而不是一次崩溃。