错误处理
Rust 的错误处理体系以其严谨性著称。它不使用传统的 try-catch 异常机制,而是通过类型系统将错误显式化,强制开发者在编译期就面对可能的失败。
一、 不可恢复错误:panic!
当程序遇到无法恢复的错误时(如数组越界或断言失败),Rust 使用 panic! 宏来终止执行。这会 unwind 栈(清理资源)或直接 abort(不清理,适合嵌入式系统)。
1. 发生什么?
- 程序打印错误信息。
- 展开(Unwinding) :Rust 沿着栈往回走,清理每个函数的数据(释放所有权)。
- 程序退出。
fn main() {
// 主动触发 panic
// panic!("这里发生了不可预见的灾难!");
let v = vec![1, 2, 3];
v[99]; // ❌ 被动触发 panic:索引越界
}
当你取到了一个不属于你的值,这在很多时候会导致程序上的逻辑 BUG! 有编程经验的人都知道这种逻辑上的 BUG 是多么难被发现和修复!因此程序直接崩溃,然后告诉我们问题发生的位置,最后我们对此进行修复,这才是最合理的软件开发流程,而不是把问题藏着掖着:
#![allow(unused)]
fn main() {
thread 'main' (12) panicked at src/main.rs:6:6:
index out of bounds: the len is 3 but the index is 99
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
}
好的,现在成功知道问题发生的位置,但是如果我们想知道该问题之前经过了哪些调用环节,该怎么办?那就按照提示使用 RUST_BACKTRACE=1 cargo run 或 $env:RUST_BACKTRACE=1 ; cargo run 来再一次运行程序
#![allow(unused)]
fn main() {
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:6:6
stack backtrace:
0: rust_begin_unwind
at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35/library/std/src/panicking.rs:517:5
1: core::panicking::panic_fmt
at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35/library/core/src/panicking.rs:101:14
2: core::panicking::panic_bounds_check
at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35/library/core/src/panicking.rs:77:5
3: <usize as core::slice::index::SliceIndex<[T]>>::index
at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35/library/core/src/slice/index.rs:184:10
4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35/library/core/src/slice/index.rs:15:9
5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35/library/alloc/src/vec/mod.rs:2465:9
6: world_hello::main
at ./src/main.rs:4:5
7: core::ops::function::FnOnce::call_once
at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35/library/core/src/ops/function.rs:227:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
}
上面的代码就是一次栈展开(也称栈回溯),它包含了函数调用的顺序,当然按照逆序排列:最近调用的函数排在列表的最上方。因为咱们的 main 函数基本是最先调用的函数了,所以排在了倒数第二位,还有一个关注点,排在最顶部最后一个调用的函数是 rust_begin_unwind,该函数的目的就是进行栈展开,呈现这些列表信息给我们。
要获取到栈回溯信息,你还需要开启 debug 标志,该标志在使用 cargo run 或者 cargo build 时自动开启(这两个操作默认是 Debug 运行方式)。同时,栈展开信息在不同操作系统或者 Rust 版本上也有所不同。
2. 何时使用?
- 示例代码或原型 :快速展示逻辑。
- 测试代码 :断言失败。
- 逻辑不可达 :你确信这段代码永远不会运行到,除非有严重的 Bug。
二、 可恢复错误:Option 与 Result
这是 Rust 错误处理的核心,通过两个枚举来包裹“可能不存在的值”或“可能失败的操作”。Rust 不使用异常,而是返回枚举类型:
- Option:表示可能为空的值。Some(T) 或 None。
- Result<T, E>:表示成功或失败。Ok(T) 或 Err(E)。
1. Option<T>:值可能不存在
用于表示一个值要么有(Some(T)),要么没有(None)。
fn find_index(target: i32, list: Vec<i32>) -> Option<usize> {
for (i, &item) in list.iter().enumerate() {
if item == target { return Some(i); }
}
None
}
fn main() {
let list = vec![10, 20, 30];
match find_index(20, list) {
Some(index) => println!("找到索引: {index}"),
None => println!("未找到"),
}
}
2. Result<T, E>:操作可能失败
用于表示一个操作要么成功(Ok(T)),要么失败(Err(E))。
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
let _file = match greeting_file_result {
Ok(file) => file,
Err(error) => panic!("打开文件失败: {:?}", error),
};
}
模式匹配和 unwrap
- match:最安全的方式。
- unwrap():如果 Ok 返回值,否则 panic!(不推荐生产环境)。
- expect(“消息”):类似 unwrap,但自定义 panic 消息。
- unwrap_or(default):为 Option/Result 提供默认值。
- unwrap_or_else(closure):懒惰计算默认值。
三、 常用组合器 (Combinators)
组合器允许你以函数式的风格链式处理 Option 和 Result,避免层层嵌套的 match。
| 组合器 | 作用描述 |
|---|---|
.map() | 仅对成功(Some/Ok)的值进行转换 |
.and_then() | 类似 map,但闭包也返回 Result/Option(自动平铺嵌套) |
.unwrap_or() | 如果失败/缺失,则返回一个默认值 |
.unwrap_or_else() | 类似 unwrap_or,但默认值通过闭包计算(延迟求值) |
.map_err() | 仅对 Err 进行转换(通常用于转换错误类型) |
fn main() {
// --- 1. .map(): 只转换成功的值,忽略失败 ---
let s = Some("5");
let n = s.map(|val| val.parse::<i32>().unwrap_or(0));
// n 现在是 Some(5)
// --- 2. .and_then(): 展平嵌套(类似 flat_map) ---
// 如果转换函数也返回 Option/Result,用 and_then 防止出现 Option<Option<T>>
let get_val = |i: i32| if i > 0 { Some(i * 2) } else { None };
let result = Some(10).and_then(get_val);
// result 是 Some(20),而不是 Some(Some(20))
// --- 3. .unwrap_or() 与 .unwrap_or_else(): 兜底默认值 ---
let x: Option<i32> = None;
let val = x.unwrap_or(0); // 如果是 None,则返回 0
// or_else 接受闭包,适合计算默认值开销较大的场景(延迟求值)
let val_lazy = x.unwrap_or_else(|| {
// 执行复杂的计算过程...
100
});
// --- 4. .map_err(): 只处理错误,不改动成功值 ---
let res: Result<i32, i32> = Err(404);
let updated_res = res.map_err(|e| format!("Error code: {}", e));
// updated_res 是 Err("Error code: 404")
println!("组合器处理结果: {:?}, {}, {:?}", n, val, updated_res);
}
四、 ? 操作符:错误传播的捷径
? 操作符是 Rust 错误传播的语法糖。它可以极大地简化代码,让逻辑保持清晰。程序几乎不太可能只有 A->B 形式的函数调用,一个设计良好的程序,一个功能涉及十几层的函数调用都有可能。而错误处理也往往不是哪里调用出错,就在哪里处理,实际应用中,大概率会把错误层层上传然后交给调用链的上游函数进行处理,错误传播将极为常见
1. 工作原理
当你在一个返回 Result 的表达式后面加 ? 时:
- 如果结果是
Ok,它会自动解包出里面的值,程序继续执行。 - 如果结果是
Err,它会立即 提前返回 (Return)整个函数,并将错误传递给调用者。
use std::fs::File;
use std::io::{self, Read};
fn read_username() -> Result<String, io::Error> {
// 如果 open 失败,直接返回 Err;如果成功,f 绑定为 File 对象
let mut f = File::open("name.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?; // 如果读取失败,直接返回 Err
Ok(s)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let username = read_username()?;
println!("用户名: {}", username);
Ok(())
}
- 解释:? 等价于
let mut file = match File::open(filename) {
Ok(f) => f,
Err(e) => return Err(e),
};
- 要求:函数必须返回
Result/Option。 - 链式使用:支持多个 ?,错误会向上传播。
From trait:如果错误类型不同,? 会自动转换(如果实现了 From)。
2. 使用限制
?只能在返回类型与?处理的类型相兼容的函数中使用(例如在返回Result的函数中处理Result)。- 在
main函数中使用?需要将main的返回类型改为Result<(), Box<dyn Error>>。
五、自定义错误类型
对于复杂应用,定义自己的错误枚举,结合 thiserror 或 anyhow crate 更加方便。
总结概括
panic!:用于 程序 Bug 。当你无法预见错误或错误会导致程序状态不可靠时使用。Option:用于 可能缺失 。不代表失败,只是“没有”。Result:用于 可能失败 。明确区分成功数据和错误信息。?:用于 传播错误 。让错误处理像写直线代码一样简单。- 组合器 :用于 优雅转换 。让数据在各种状态间流动而不需要嵌套判断。