用类型表达:没有 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`」,调用方拿到的是一个值而不是一次崩溃。