📚 Rust 课程系列

  1. 课程概览
  2. 基础语法
  3. 函数与输入输出
  4. 所有权、借用与生命周期
  5. 结构体、枚举与模式匹配
  6. 类型系统:泛型、trait 与多态
  7. 集合与容器
  8. 错误处理与 Panic 恢复(本文)
  9. 模块、属性与宏
  10. 智能指针、迭代器与闭包
  11. 并发与异步编程
  12. Unsafe Rust 与常用 trait 详解
  13. 工具链、Cargo 与外部 crate
  14. 最佳实践、性能与调试

Rust 把错误分成两类:可恢复错误用 Result<T, E> 表达,交由调用方决定如何处理;不可恢复错误(违反不变式、无法继续)用 panic! 终止当前线程。本章先讲 Option/Result/? 与组合子,再讲 panic 的行为、捕获与断言,帮你建立「何时该 panic、何时该 Result」的判断。

错误处理

Rust 没有 try/catch,而是用类型表达「可能失败」:

类型含义变体
Option<T>可能为空Some(T) / None
Result<T, E>可能出错Ok(T) / Err(E)

🔄 对比:Rust 的 Result ≈ Go 的 (value, error) 多返回值,但编译器通过 ? 强制你处理错误,而非靠约定检查 err != nil

1
2
3
4
5
6
7
// Option: 解包或提供默认值
let val = Some(5).unwrap_or(0); // 5

// Result: 成功返回值,失败返回错误
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 { Err("Divide by zero".into()) } else { Ok(a / b) }
}

? 运算符与错误传播

? 的语义:成功则解包继续,失败则提前 return Err(...)。它还会通过 From trait 自动把底层错误转换成当前函数的返回错误类型,因此不同来源的错误可以统一汇聚到一个 Error 枚举:

1
2
3
4
5
6
7
8
9
use std::fs::File;
use std::io::Read;

fn read_config(path: &str) -> Result<String, MyError> {
let mut f = File::open(path)?; // io::Error -> MyError (通过 From)
let mut s = String::new();
f.read_to_string(&mut s)?; // 同上
Ok(s)
}

💡 提示? 只能用在返回 Result/Option 的函数中。在 main 里使用需要 main 返回 Result<(), E>(Rust 2018+ 支持)。

⚠️ 注意? 会静默转换错误类型。如果 From 实现有误,可能把关键错误信息吞掉——确保 From 实现保留原始错误链(推荐用 thiserror#[from] 自动生成)。

库 vs 应用的错误类型策略

场景推荐方案理由
库(library)精确 enum Error + thiserror让调用方能按分支处理
应用(binary)anyhow::Erroreyre::Report只关心「失败就报错退出」,省事

thiserror#[derive(Error)] 简化自定义错误枚举的 Display/From 实现;anyhow 提供 .context() 方法附带额外上下文,便于排查问题。

💡 提示:需要附带上下文时,用 map_erranyhow.with_context(|| ...) 而非裸 ?,这样错误信息才有定位价值。

组合子方法

组合子让你像流水线一样处理 Option/Result,避免层层 match

1
2
3
4
5
6
7
8
9
10
11
// Option 链式组合
let sum = Some(5).and_then(|x| Some(10).map(|y| x + y)); // Some(15)

// Result 映射与错误转换
let doubled: Result<i32, &str> = Ok(5).map(|x| x * 2); // Ok(10)

fn double_number(s: &str) -> Result<i32, String> {
s.parse::<i32>()
.map(|n| n * 2)
.map_err(|e| format!("解析失败: {}", e))
}

常用组合子速查:

方法作用适用
map转换成功值Option/Result
map_err转换错误值Result
and_then链式操作(flatMap)Option/Result
unwrap_or失败时提供默认值Option/Result
unwrap_or_else失败时用闭包计算默认值Option/Result
unwrap_or_default失败时用 DefaultOption/Result

何时 panic、何时 Result

经验法则:

  • 调用方应当能合理处理的失败 -> Result
  • 不变量被破坏、继续执行会破坏数据安全 -> panic!(如 unwrap 一个「按约定一定有值」的 Option
  • 库内部的失败应尽量用 Result,不要替调用方决定「致命」

💡 提示:一个简单的判断——「如果调用方看到这个错误,能做有意义的事吗?」能 -> Result;不能 -> panic

Panic 与错误恢复

panic 表示「发生了无法恢复的编程错误」,默认行为是展开栈(unwinding)——逐层调用 Drop 清理资源后线程退出。

🔄 对比:Rust 的 panic ≈ C++ 的 std::terminate / Go 的 panic,但 Rust 默认只终止当前线程而非整个进程(除非 panic = "abort")。

unwrap / expect 系列

1
2
3
4
5
let val = Some(5).unwrap();                    // 5; None 时 panic
let val = Some(5).expect("Expected a value"); // 同上,但 panic 消息更明确
let val = None.unwrap_or(0); // 0; 不 panic
let val = None.unwrap_or_else(|| 10 * 2); // 20; 惰性计算默认值
let val: String = None.unwrap_or_default(); // ""; 使用 Default trait

⚠️ 注意unwrap/expect 适合「按约定一定成功」的场景(如刚 insert 过就立刻 getHashMap)以及测试/原型代码。生产代码里对可能失败的输入应改用 ?unwrap_or 系列。

Panic 时中止(进阶)

1
2
3
# Cargo.toml
[profile.release]
panic = "abort" # 跳过栈展开,减小二进制体积
1
2
3
4
std::panic::set_hook(Box::new(|info| {
println!("Panic: {}", info);
std::process::exit(1);
}));

⚠️ 注意panic = "abort" 跳过栈展开直接终止进程,可减小二进制体积(无需展开表),但代价是 Drop 不会被调用、也无法被 catch_unwind 捕获。

🔬 进阶:嵌入式和 WASM 目标默认 panic = "abort",因为展开表占用空间且目标平台可能不支持展开。

捕获 Panic

1
2
3
4
5
6
7
8
9
10
11
12
use std::panic::catch_unwind;

let result = catch_unwind(|| {
// may panic
if rand::random() { panic!("Oops!"); }
42
});

match result {
Ok(value) => println!("Success: {}", value),
Err(_) => println!("Caught panic"),
}

⚠️ 注意catch_unwind 只能捕获展开(unwinding)的 panic,不能捕获 panic = "abort"abort 发出的 panic。

catch_unwind 主要用于 FFI 边界(C 调用 Rust 时不能让 panic 跨越边界)或在「希望一个子任务失败不影响整体」时隔离故障。不应当作常规错误处理手段——能用 Result 就用 Result

🔬 进阶:跨 FFI 边界的 panic 是 UB(未定义行为)。如果你的库可能被 C 代码调用,在 Cargo.toml 中加 panic = "unwind" 并在 FFI 函数入口用 catch_unwind 包裹,或使用 #[unwind(aborts)](nightly)。

Assert 宏

断言失败会 panic,常用于测试与不变式检查:

1
2
3
4
debug_assert!(x > 0);                                    // 仅 debug 构建检查
assert!(x > 0, "x must be positive, got {}", x); // 始终检查
assert_eq!(a, b); // 相等断言
assert_ne!(a, b); // 不等断言

💡 提示debug_assert! 在 release 构建中被移除,适合「昂贵的、仅用于调试期发现逻辑错误」的检查;正式不变式应使用 assert!,确保始终生效。

🔄 对比debug_assert! ≈ C/C++ 的 assert() 宏(NDEBUG 时移除),但 Rust 的 assert! 始终生效,不像 C 的 assert 在 release 中默认消失。