Option 类型与空值处理
本教程共 78 篇 · 第 42 篇 · 更新于 2026-08-08 · 约 9 分钟阅读
本节目标:理解 Rust 为何消灭 null,用
Option<T>显式表达”有值或无值”,并掌握 match 解构与常用方法安全地取值。
很多语言里有个 null,表示”这里什么都没有”。它看似方便,却是无数崩溃和 bug 的源头——你以为拿到的是值,实际是个空指针,一用就炸。图灵奖得主 Tony Hoare 在 2009 年公开道歉,称自己当年发明 null 是”十亿美元的错误”,因为它引发的空指针异常几十年来坑了无数程序员。
Rust 直接不提供 null,从根上杜绝这类问题。那”可能没有值”怎么表达?答案是 Option<T>——把”可能有值也可能没有”这件事,明明白白写进类型里。
1-1 Option 是什么
Option 是个枚举,定义只有两个成员:
enum Option<T> {
None, // 没有值
Some(T), // 有值,T 装在里面
}
一句话:一个变量要么有值 Some(T),要么为空 None。Option、Some、None 都在 prelude 里,直接用名字即可,不用写 Option::Some。
它解决的核心问题是:变量到底有没有值,必须写在类型里。编译器强迫你在编译期就处理”空”的情况,而不是运行时才爆雷。这叫”用类型把 null 关进笼子”。
1-2 用 match 解构 Option
最基础的处理方式是 match,把 Some 和 None 都覆盖:
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five); // Some(6)
let none = plus_one(None); // None
plus_one 收一个 Option<i32>,返回也是 Option<i32>。这种”进 Option、出 Option”的函数在标准库里到处都是。
匹配时,None 分支直接返回 None 不做处理;Some(i) 用模式把里面的值绑到 i,加一后用 Some 重新包好返回。注意 Some(i) 的 i 是通过模式匹配自动解出来的,不需要你手动拆。
Tip因为
match要求穷尽,你没法只写Some漏掉None——编译器逼你把”空”也考虑进去。这就是 Option 比 null 安全的地方:空值不再是一个能悄悄溜过的意外。
1-3 常用取值方法
除了 match,Option 还提供一组方便的方法来取值,日常很常用:
let x = Some(5);
let y: Option<i32> = None;
// 是否为有值 / 无值
assert!(x.is_some());
assert!(y.is_none());
// 有值就取出,无值给默认
let a = x.unwrap_or(0); // 5
let b = y.unwrap_or(0); // 0
// 有值就对其中的值做变换,无值保持 None
let c = x.map(|v| v * 2); // Some(10)
// 取出值,无值就 panic(仅原型/测试用)
let d = x.unwrap(); // 5
几个要点:
is_some()/is_none():判断有没有值;unwrap_or(默认):有值取出,没值返回你给的默认值,最安全;map(f):有值就把里面的T传给f处理、结果重新包成Some,没值就原样None;unwrap()/expect():和Result上的一样,有值取出、没值直接 panic,只适合原型或确信有值的时候。
Warning
unwrap()在None时会 panic。正式代码里如果”没值”是可能出现的正常情况,请用unwrap_or或match处理,别用unwrap赌运气。
1-4 还有几个实用方法
let mut x = Some(5);
// 取出里面的值并置为 None(消费式)
let taken = x.take(); // taken = Some(5), x 变成 None
let mut y = Some(3);
// 有值才执行闭包
y.inspect(|v| println!("值是 {v}"));
// and_then:链式处理,只有有值时才继续
let z = Some(2).and_then(|v| Some(v * 10)); // Some(20)
take 常用于”把值从某个结构里拿走”的场景;and_then 类似 map,但闭包返回的是 Option,适合连续的多步可选计算。
1-5 Option 与 ? 的配合
上一章讲过,? 也能用在 Option 上:遇到 None 直接返回 None。这让它和 Result 一样好用:
fn last_char_of_first_line(text: &str) -> Option<char> {
text.lines().next()?.chars().last()
}
.next() 返回 Option:是 None 就整体返回 None 不再继续;是 Some 才接着处理。这种”链式里随时提前返回”的写法非常清爽。
1-6 为什么 Option 比 null 好
用 null 的语言里,任何东西都可能偷偷是空,编译器帮不了你,只能在运行时祈祷。Rust 把”可能有值”这件事写进类型系统:一个 i32 永远有值,一个 Option<i32> 才可能为 None。你拿到 i32 时不必担心它是空,拿到 Option 时编译器逼你处理空。
类型上区分清楚,开发者心里也有底:看见 Option 就警觉”这里可能没值”,看见普通类型就放心用。这种显式的代价,换来的是大量空指针类 bug 在编译期就被消灭。
1-7 Option 的组合子:把嵌套一层层摊平
除了 match 和 unwrap_or,Option 还有一组”组合子”方法,专门用来把多个可能为空的操作串起来,避免一层套一层的 match。
最常用的是 map:有值时对其变换,无值时原样透传 None。and_then 更特别:它的闭包本身返回 Option,所以适合”两步都可能失败”的链式查询,而不会得到 Some(Some(x)) 这种嵌套。
let n: Option<u32> = Some(3);
let doubled = n.map(|x| x * 2); // Some(6)
let nested = n.map(|x| Some(x + 1)); // Some(Some(4)) 嵌套了
let flat = n.and_then(|x| Some(x + 1)); // Some(4) 被摊平
let none: Option<u32> = None;
let fallback = none.or(Some(99)); // 左边是 None 时取右边:Some(99)
or / or_else 用来提供兜底值;flatten 能把 Option<Option<T>> 压成 Option<T>。这些组合子配上迭代器(后面章节会讲)非常好用,让”空值”不再打断你的逻辑流。
Tip当心:用
map得到Option<Option<T>>时,记得用and_then或flatten摊平,否则后续代码会一直背着两层Some。
1-8 Option 与默认值:unwrap_or 家族
取 Option 里的值时,除了 unwrap(没有就 panic),还有一组”温柔”的兜底方法。最常用的是 unwrap_or:有值就用值,没有就给一个备胎。
let maybe: Option<u32> = None;
let n = maybe.unwrap_or(0); // 没值,用 0
如果备胎算起来很贵,用 unwrap_or_else(|| 计算) 做惰性求值——只有真的没有值时才跑那段计算;有值时根本不会执行,省开销。unwrap_or_default() 则要求 T 实现了 Default(比如 String 的默认是空串、Vec 的默认是空数组),直接给类型的”空状态”。
另外 get_or_insert 作用在 &mut Option<T> 上:有值返回引用,没值先插入默认值再返回引用,常用来”懒初始化”一个字段。
Note选哪个取决于”没有值时该怎么办”:固定备胎用
unwrap_or,贵备胎用unwrap_or_else,要类型默认空状态用unwrap_or_default,而这些本质上都比unwrap安全,因为不会让程序崩溃。
1-9 你的 API 该返回 Option 还是 Result
设计自己的函数时,常纠结”该返回 Option 还是 Result”。一条好记的规则是:如果”没有结果”本身就是一个合理、常见、调用方只需知道”有没有”的状态,就用 Option;如果”失败”且”为什么失败”对调用方很重要,就用 Result。
翻翻标准库就能印证:HashMap::get 返回 Option,因为”键不在”是很正常的事,调用方往往只关心”有没有”;parse 返回 Result,因为”解析失败”时你通常想知道”到底哪里不合法”;iter().next() 在空迭代器上返回 Option,因为”没有下一个”是预期内状态。
所以写 API 时先问自己:这个函数”没结果”算不算正常?算,就 Option;不算、但可能出错且原因重要,就 Result。这能让调用方用最贴合的方式处理,也避免拿 Result 去装”本来就该可能为空”的语义。
Note当”没有”和”出错”同时存在时(比如”按 id 查用户,用户可能不存在,也可能数据库连不上”),通常把”不存在”建模成
Option,把”连接出错”建模成Result,再用?和ok_or把它们组合清楚。
1-10 Option 其实早已无处不在
你之前几章其实一直在用 Option,只是可能没意识到。回头看几个老朋友:
Vec::get(索引)返回Option<&T>:越界时是None,不 panic;HashMap::get(键)返回Option<&V>:键不存在时是None;- 迭代器的
.next()在取完所有元素后返回None,这正是for循环能停下来的原因; - 上一章的
?用在Option上,遇到None直接提前返回。
let v = vec![10, 20, 30];
let first = v.get(0); // Option<&i32>:有值 -> Some(&10)
let out = v.get(99); // Option<&i32>:越界 -> None
if let Some(x) = first {
println!("第一个元素是 {x}");
}
把这几个散落的用法串起来你会发现:Option 不是某个孤立的语法,而是 Rust 表达”可能为空”的统一语言。从集合取值、到迭代、到错误处理,处处是它。习惯了”看见 Option 就先想 None”的思维方式,空值 bug 几乎无机可乘。
Note一个小提醒:直接对
Option用==比较是可以的,Some(1) == Some(1)是true、Some(1) == None是false。但比较的是”整体是否相等”,包括”有没有值”这一层。
1-11 小结
- Rust 没有 null,用
Option<T>(Some(T)/None)显式表达”可能有值”; - 用
match解构,穷尽匹配逼你处理None; - 常用方法:
is_some/is_none、unwrap_or、map、take、and_then; unwrap/expect遇None会 panic,只用于原型或确信有值;?可用于Option,遇None提前返回;- 类型上区分”一定有值”和”可能为无”,让空值 bug 在编译期现形。
下一章进入泛型,看看怎么写出”一套代码适配多种类型”的函数和结构体。