Rust 错误处理
Rust 没有 try/catch,这是它和其他语言最显眼的区别之一。它把错误分成两类——不可恢复的 panic 和可恢复的 Result,分别用不同机制处理。这套设计让 Rust 代码的健壮性远超 try/catch 风格的语言,因为错误是类型,编译器强迫你处理。这一篇我们讲透。
1. 两类错误,两套机制
Rust 的错误处理哲学:
// Rust 把错误分成两类,用不同机制处理:
// 1. 不可恢复错误(panic)
// 程序进入了"坏状态",无法继续。用 panic! 触发,
// 会打印错误信息、展开栈、退出程序。
fn main() {
let v = vec![1, 2, 3];
// let x = v[10]; // 运行时 panic! 数组越界
panic!("自定义崩溃"); // 主动触发 panic
}
// 2. 可恢复错误(Result)
// 操作可能失败,但有合理的恢复方式(如重试、提示用户)。
// 用 Result<T, E> 枚举表示:
// Ok(T) —— 成功,包含值 T
// Err(E) —— 失败,包含错误 E
enum Result<T, E> {
Ok(T),
Err(E),
}
// 设计哲学:
// panic 用于"程序员的 bug"(数组越界、违反不变量)
// Result 用于"运行时可能失败的操作"(读文件、网络、解析)
// 没有 try/catch,错误是类型,必须显式处理核心理解:
- panic(不可恢复):程序进入坏状态,无法继续。比如数组越界、违反不变量。会打印栈、退出进程。
- Result(可恢复):操作可能失败但有合理恢复方式。比如读文件、网络请求、解析数字。用 Result<T, E> 枚举表示。
对比 try/catch:Java 把所有错误都用异常表示,你"可能"忘记 catch;Rust 把可恢复错误做成类型 Result,编译器强制你处理 Err 分支——不会"忘记"。
2. Result 类型
Result 是标准库提供的枚举(上一篇讲枚举时见过),Ok(T) 表示成功带值,Err(E) 表示失败带错误:
use std::fs;
use std::io;
use std::num::ParseIntError;
fn main() {
// 打开文件可能失败:返回 Result
let content: Result<String, io::Error> = fs::read_to_string("config.txt");
// 必须显式处理 Err 的情况(类型系统强制)
match content {
Ok(text) => println!("文件内容: {}", text),
Err(e) => eprintln!("读取失败: {}", e),
}
// 解析数字可能失败:返回 Result
let n: Result<i32, ParseIntError> = "42".parse();
match n {
Ok(num) => println!("解析成功: {}", num),
Err(e) => println!("解析失败: {}", e),
}
// 链式调用:map / and_then 处理成功情况
let result = "100"
.parse::<i32>() // Result<i32, _>
.map(|n| n * 2) // 成功则乘以 2
.map(|n| n + 1);
println!("{:?}", result); // Ok(201)
}几个要点:
- 必须处理 Err:Result 是枚举,不用就编译警告。这是类型系统强迫的。
- match 是基础:模式匹配 Ok/Err 两种情况。
- map / and_then:链式处理成功情况,函数式风格。错误自动传播。
- 标准库大量返回 Result:文件 IO、网络、解析、锁获取等,所有可能失败的操作。
3. 问号运算符 ?(关键!)
这是 Rust 错误处理的杀手锏。? 让错误传播像写同步代码一样自然——成功就解包值,失败就立即 return Err:
use std::fs;
use std::io;
use std::num::ParseIntError;
// ? 运算符:错误传播的语法糖
// 成功就解包出值,失败就立即返回 Err
fn read_config() -> Result<i32, io::Error> {
// 不用 ? 的写法(啰嗦):
let content = match fs::read_to_string("config.txt") {
Ok(c) => c,
Err(e) => return Err(e), // 手动传播
};
// 用 ? 的写法(简洁!):
let content = fs::read_to_string("config.txt")?;
// ↑ 等同上面的 match:
// 成功 -> content 拿到 String
// 失败 -> 函数立即 return Err(e)
Ok(42)
}
// ? 能让多层错误传播像写同步代码一样自然
// 没有 ? 时:层层 match,代码又长又难看
// 有了 ?:像普通赋值一样,错误自动向上传播
// ? 还能做错误类型转换(From trait)
// 函数返回 io::Error,但 parse 返回 ParseIntError
// ? 会自动调用 From 把 ParseIntError 转成 io::Error
fn read_and_parse() -> Result<i32, io::Error> {
let text = fs::read_to_string("num.txt")?;
let n: i32 = text.trim().parse()?; // ParseIntError -> io::Error
Ok(n)
}? 的魔力:
- 语法糖:等同于 match + return Err,但极其简洁。
- 自动类型转换:通过 From trait,? 自动把子错误转成函数返回的错误类型。这是大量样板代码的克星。
- 也能用在 Option:对 Option 用 ? 表示"None 就提前返回 None"。常用于提取嵌套 Optional 值。
这是写 Rust 日常代码最重要的运算符之一。几乎所有可能失败的操作都用 ? 传播,代码读起来像没有错误处理的同步代码,但实际是健壮的。
4. unwrap 与 expect(谨慎使用)
unwrap 系列方法是 Result 的"快捷解包":成功拿值,失败 panic。它们简化代码但牺牲健壮性:
fn main() {
// unwrap:解包 Result,成功拿值,失败 panic
let n: i32 = "42".parse().unwrap(); // OK,n = 42
println!("{}", n);
// let bad: i32 = "abc".parse().unwrap(); // panic!
// expect:同 unwrap,但能自定义错误信息
let n: i32 = "42"
.parse()
.expect("应该是数字啊");
println!("{}", n);
// unwrap_or / unwrap_or_else:失败时给默认值(不 panic)
let n: i32 = "abc".parse().unwrap_or(0); // 失败返回 0
let n: i32 = "abc".parse().unwrap_or_else(|_| -1); // 失败返回 -1
println!("{}", n);
// 何时用 unwrap / expect?
// - 写示例、原型、单元测试 -> 可以用
// - 你 100% 确定不会失败 -> 可以用(但加注释说明)
// - 生产代码、用户输入、IO 操作 -> 千万别用,用 ? 或 match
}使用建议:
- 原型/示例/测试:可以用 unwrap/expect,代码简洁。
- 100% 确定不会失败:可以用,但加注释说明。
- 生产代码:避免,用 ? 或 match。生产代码用 unwrap 等于"挖坑"。
- unwrap_or / unwrap_or_else:失败给默认值,不 panic。这种是安全的,生产可用。
5. 自定义错误类型
当应用有多种错误来源(IO、解析、业务逻辑),通常定义一个统一错误类型。配合 From trait,让 ? 自动转换:
use std::fmt;
use std::error::Error;
// 自定义错误类型:实现 Debug + Display + Error trait
#[derive(Debug)]
enum AppError {
Io(std::io::Error),
Parse(std::num::ParseIntError),
NotFound(String),
Unauthorized,
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AppError::Io(e) => write!(f, "IO 错误: {}", e),
AppError::Parse(e) => write!(f, "解析错误: {}", e),
AppError::NotFound(name) => write!(f, "未找到: {}", name),
AppError::Unauthorized => write!(f, "未授权"),
}
}
}
impl Error for AppError {} // 默认实现就够
// 实现 From,让 ? 能自动转换
impl From<std::io::Error> for AppError {
fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}
impl From<std::num::ParseIntError> for AppError {
fn from(e: std::num::ParseIntError) -> Self { AppError::Parse(e) }
}
// 现在函数可以返回统一错误类型,? 自动转换各种子错误
fn do_work() -> Result<i32, AppError> {
let text = std::fs::read_to_string("num.txt")?; // io::Error -> AppError
let n: i32 = text.trim().parse()?; // ParseIntError -> AppError
Ok(n)
}
// 实战中推荐用 thiserror crate(自动生成上面的样板代码)
// 或 anyhow crate(用于应用层,不关心具体错误类型)关键点:
- 统一错误类型:让函数返回
Result<T, AppError>,调用者只需处理一种错误。 - From trait:实现
From<SubError>后,? 自动把子错误转成 AppError。 - 实战推荐:手写上面的样板代码很啰嗦,实际项目用 thiserror crate(自动生成)或 anyhow crate(应用层用,不关心具体类型)。
6. thiserror 与 anyhow(实战利器)
两个最流行的错误处理 crate:
- thiserror:用于库。通过
#[derive(Error)]自动生成自定义错误类型的样板代码。提供清晰的错误类型 API。 - anyhow:用于应用。不关心具体错误类型时,用 anyhow 提供的 Result 类型,任何错误都能装。语法极简,适合 CLI、Web 后端等"能跑就行的应用层"。
经验:库的代码用 thiserror 提供精确的错误类型;应用代码用 anyhow 简化错误传播。这是 Rust 社区的常见组合。
7. panic 详解
panic 表示"不可恢复",触发后会展开调用栈(默认)或直接终止。看下细节:
fn main() {
// panic! 触发不可恢复错误
// 适用场景:
// - 数组越界、违反不变量(理论上不该发生)
// - 程序员逻辑错误(不是用户输入问题)
// - 初始化失败(程序无法继续运行)
let v = vec![1, 2, 3];
if let Some(x) = v.get(10) {
println!("{}", x);
} else {
// 这里如果继续运行会出错,可以 panic
// panic!("数组索引错误");
}
// 常见自动 panic 的场景:
// - 数组越界 v[10] (用 .get() 避免)
// - unwrap 失败
// - 整数溢出(debug 模式)
// - 除以零
// - 显式 unreachable!() / todo!() / unimplemented!()
// panic 的行为:
// - 默认:展开栈(unwind),清理资源
// - 可配置:直接终止(abort),体积更小
// 在 Cargo.toml: [profile.release] panic = "abort"
}
// panic vs Result 怎么选?
// - "用户的错"(输入错、文件不存在、网络断) -> Result
// - "程序员的错"(逻辑不可能出现的情况) -> panicpanic 的两种栈处理:
- unwind(默认):展开栈,运行析构函数,清理资源。体积略大。
- abort:直接终止,体积更小。嵌入式、追求小体积时用。
8. 错误处理最佳实践
- 优先 Result 而非 panic:用户输入、IO、网络等"可预期失败"用 Result。
- 善用 ?:错误传播是日常,不要层层 match。
- 定义清晰错误类型:让调用者能 match 具体错误分支处理。
- 错误信息要有上下文:用
map_err方法给错误附加提示(如"读取配置失败"),排查更容易。 - 不要吞掉错误:不用
let _ = may_fail();,要么处理,要么用 ? 传播。 - main 也可以返回 Result:
fn main() -> Result<(), Box<dyn Error>>,失败时打印错误并退出。
9. 实战:写一个健壮的文件读取
综合应用,这是典型场景:
- 读文件用标准库 fs 模块的
read_to_string函数,返回 Result。 - 解析内容用
parse,返回 Result。 - 每步用
?传播错误,加 map_err 添加上下文。 - 最终函数返回
Result<T, AppError>,调用者一个 match 搞定。 - 这种代码生产级别健壮——所有错误都被处理,且代码依然简洁。
小结
这一篇你掌握了 Rust 的错误处理体系:panic(不可恢复,程序员错误)、Result<T, E>(可恢复,类型化错误)、问号 ?(优雅传播)、unwrap/expect(谨慎用)、自定义错误 + From(统一错误处理)、thiserror/anyhow(实战利器)。Rust 的"错误即类型"哲学,让代码的健壮性远超 try/catch 风格——错误不会被"忘记处理"。
下一篇是系列最后一篇——模块与 Crate,讲解 Rust 如何组织大型项目的代码。
← 上一篇 Rust Trait 特征
下一篇 Rust 模块与 Crate →