Skip to content

Result、Option与错误处理

1. 这是什么

在 Rust 里,Option<T>Result<T, E> 是最核心的两类“结果表达方式”。

它们分别在回答两种不同问题:

  • Option<T>这里可能有值,也可能没值
  • Result<T, E>这里可能成功,也可能失败

一句话理解:

  • Option 处理“缺失”
  • Result 处理“错误”

Rust 不鼓励把这些情况偷偷塞进 null、魔法值或者模糊状态里,而是要求你把可能性显式写出来,再决定如何处理。

2. 为什么重要

很多语言里,初学者最容易踩的坑之一就是:

  • 以为这里一定有值,结果拿到 null
  • 以为这里一定成功,结果运行时报错
  • 错误信息一路丢失,最后只剩一个模糊异常

Rust 的做法是把这些不确定性前置到类型系统里。
也就是说,编译器会不断提醒你:

  • 这里的值不一定存在
  • 这里的操作不一定成功
  • 你必须决定怎么处理

这会让代码一开始显得更啰嗦一点,但换来的好处是:

  • 错误路径更清晰
  • 边界更明确
  • 运行期“突然炸掉”的概率更低

3. 先建立直觉

3.1 Option 的直觉

rust
let name: Option<String> = Some(String::from("Rust"));
let empty: Option<String> = None;

这里表示:

  • name 里有值
  • empty 里没值

重点不在于记住 Some / None 这两个名字,而在于:

  • “有没有值”本身已经进入类型定义了
  • 你不能假装它一定存在

3.2 Result 的直觉

rust
let ok: Result<i32, &str> = Ok(42);
let err: Result<i32, &str> = Err("something went wrong");

这表示:

  • 成功时拿到一个 i32
  • 失败时拿到一个错误信息

也就是说,成功和失败都是正常返回路径的一部分,而不是“藏在别处再爆炸”。

4. 核心内容

4.1 Option 适合处理“值可能不存在”

例如从数组里安全取值:

rust
fn first_item(list: &[i32]) -> Option<i32> {
    if list.is_empty() {
        None
    } else {
        Some(list[0])
    }
}

这段代码的好处是非常明确:

  • 有值就返回 Some(...)
  • 没值就返回 None

调用方必须决定怎么处理这个“可能为空”的结果。

4.2 Result 适合处理“操作可能失败”

例如解析数字:

rust
fn parse_number(s: &str) -> Result<i32, std::num::ParseIntError> {
    s.parse::<i32>()
}

这里的语义非常清楚:

  • 如果解析成功,返回 Ok(i32)
  • 如果失败,返回 Err(...)

这比“失败时返回 -1”之类魔法值要稳健得多。

4.3 用 match 显式处理

最基础的写法是 match

rust
fn print_name(name: Option<String>) {
    match name {
        Some(value) => println!("name = {}", value),
        None => println!("没有名字"),
    }
}

以及:

rust
fn show_result(result: Result<i32, &str>) {
    match result {
        Ok(value) => println!("成功:{}", value),
        Err(err) => println!("失败:{}", err),
    }
}

这类写法虽然直接,但最适合建立直觉,因为它把每一种情况都展开给你看了。

4.4 if let 和简化处理

如果你只关心一种情况,可以用 if let

rust
if let Some(name) = maybe_name {
    println!("{}", name);
}

或者:

rust
if let Err(err) = result {
    println!("error: {}", err);
}

它适合“我只抓某个分支,其他情况先略过”的场景。

4.5 unwrapexpect 要谨慎

Rust 里常见的初学写法是:

rust
let x = maybe_value.unwrap();

或者:

rust
let x = result.expect("解析失败");

它们的语义是:

  • 我坚信这里一定有值 / 一定成功
  • 如果不是,就直接 panic

这不是不能用,但要非常明确场景。

比较适合的时候:

  • demo / 临时实验
  • 测试代码
  • 你能严格保证前置条件成立

不适合的时候:

  • 正式业务路径
  • 外部输入不可信
  • 文件 / 网络 / 解析这类本来就高风险的操作

初学阶段最容易犯的错,就是把 unwrap() 当成“先让代码跑起来”的通用按钮。

4.6 ?:让错误向上返回

Rust 错误处理里一个非常关键的工具是 ?

rust
fn read_id() -> Result<i32, std::num::ParseIntError> {
    let text = "42";
    let id = text.parse::<i32>()?;
    Ok(id)
}

它的直觉含义可以理解成:

  • 如果当前这一步成功,就继续往下执行
  • 如果失败,就直接把错误向上返回

这样可以避免你层层手写 match,让错误传递更自然。

但要记住,? 不是忽略错误,而是把错误处理决策交给上层。

4.7 Option 和 Result 的区别不要混

一个很重要的判断是:

  • 没有值,是一种正常情况吗?
  • 还是说这其实代表出了错?

如果“没有值”本来就是预期内正常分支,就更适合 Option
如果这是失败、异常、约束被破坏,就更适合 Result

例如:

  • 查字典没找到某个 key:常常可用 Option
  • 读取配置文件失败:通常更像 Result

把这两者分清楚,API 语义会清晰很多。

4.8 错误处理不只是“别崩”,还是“信息别丢”

好的错误处理不只是避免 panic,还要尽量保留上下文。
也就是说,真正值得养成的习惯是:

  • 让调用方知道失败了
  • 尽量保留错误原因
  • 不要把不同错误都抹平成一个模糊结果

否则上层虽然知道失败,但不知道为什么失败,排查成本还是很高。

5. 常见误区

5.1 误区一:Option 和 Result 只是写法不同

不是。
它们表达的是两类不同语义:

  • Option 是值缺失
  • Result 是操作失败

如果你混着用,接口会变得含糊。

5.2 误区二:编译不过就 unwrap

这是 Rust 初学最常见坏习惯之一。
unwrap() 当然很方便,但它是把“不确定性”硬压成“出错就崩”。

短期看省事,长期看会让代码边界越来越脆。

5.3 误区三:错误处理就是写很多 match

match 是基础,但不是唯一方式。
真正要建立的是错误流动意识:

  • 哪些地方在产生错误
  • 哪些地方在传递错误
  • 哪些地方应该真正决定怎么处理

5.4 误区四:所有失败都该 panic

panic! 更像“程序已经进入不可恢复状态”。
而大多数业务失败、输入错误、I/O 失败,其实都应该是可处理的正常分支。

6. 一个更实用的判断思路

写 Rust 时如果碰到“不确定性”,可以先问自己:

  1. 这里是“可能没有值”,还是“操作可能失败”
  2. 如果失败,调用方需不需要知道原因
  3. 这里应该显式处理,还是把错误继续向上返回
  4. 我现在用 unwrap(),是真的安全,还是只是为了省事
  5. 这个 API 返回 Option 还是 Result,语义更清晰

这样大多数错误处理设计都会更稳。

7. 学习建议

建议按这个顺序练:

  1. 先熟悉 Some / None
  2. 再熟悉 Ok / Err
  3. match 手写处理几次
  4. 再学 if let
  5. 再学 ? 如何让错误向上传递
  6. 最后再看更完整的错误类型设计

顺序对了,后面很多标准库和第三方库 API 都会突然更容易读。

8. 自测标准

  • 能解释 OptionResult 各自解决什么问题
  • 能区分“值缺失”和“操作失败”两种语义
  • 能用 match 正确处理 Some/NoneOk/Err
  • 能说清 unwrap() 为什么不能滥用
  • 能理解 ? 的基本作用是把错误向上返回