第09章:错误处理
第09章:错误处理
9.1 两种错误处理策略
Rust 将错误分为两大类:
| 类型 | 场景 | 关键字 | 是否可恢复 |
|---|
| 可恢复错误 | 文件不存在、网络超时 | Result<T, E> | ✅ 可以处理 |
| 不可恢复错误 | 索引越界、逻辑错误 | panic! | ❌ 程序终止 |
9.2 panic! 与不可恢复错误
直接 panic
fn main() {
// 直接调用 panic!
// panic!("发生了严重错误!");
// 数组越界会导致 panic
let v = vec![1, 2, 3];
println!("{}", v[99]); // panic: index out of bounds
}
获取回溯信息
# 设置环境变量获取完整回溯
RUST_BACKTRACE=1 cargo run
# 或使用 full 模式
RUST_BACKTRACE=full cargo run
何时使用 panic
| 场景 | 是否 panic | 说明 |
|---|
| 代码中有 bug | ✅ | 逻辑错误,如索引越界 |
| 用户输入错误 | ❌ | 应返回 Result |
| 文件不存在 | ❌ | 应返回 Result |
| 网络超时 | ❌ | 应返回 Result |
| 初始化失败 | ✅ 可选 | 如果程序无法运行 |
| 不可达代码 | ✅ | 用 unreachable!() |
9.3 Result 与可恢复错误
基本用法
use std::fs::File;
use std::io::{self, Read};
fn main() {
// File::open 返回 Result<File, io::Error>
let result = File::open("hello.txt");
let file = match result {
Ok(file) => {
println!("文件打开成功");
file
}
Err(error) => match error.kind() {
io::ErrorKind::NotFound => {
println!("文件不存在,创建新文件...");
File::create("hello.txt").expect("创建文件失败")
}
io::ErrorKind::PermissionDenied => {
panic!("没有权限访问文件");
}
other => {
panic!("打开文件失败: {:?}", other);
}
},
};
}
unwrap 与 expect
use std::fs::File;
fn main() {
// unwrap: Ok 返回值,Err 则 panic
let f = File::open("test.txt").unwrap();
// expect: 与 unwrap 相同,但可以自定义 panic 消息
let f = File::open("test.txt").expect("无法打开 test.txt");
}
| 方法 | Ok 时 | Err 时 | 推荐使用场景 |
|---|
unwrap() | 返回值 | panic | 原型/测试代码 |
expect("msg") | 返回值 | panic(带消息) | 明确知道不会出错时 |
unwrap_or(val) | 返回值 | 返回默认值 | 需要默认值时 |
unwrap_or_default() | 返回值 | 类型的默认值 | 类型实现了 Default |
unwrap_or_else(f) | 返回值 | 调用闭包 | 默认值计算昂贵时 |
unwrap_or_default() | 返回值 | 默认值 | 需要默认值 |
? 操作符(错误传播)
use std::fs::File;
use std::io::{self, Read};
// 使用 ? 操作符传播错误
fn read_file_contents(path: &str) -> Result<String, io::Error> {
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
// 等价的 match 写法
fn read_file_contents_verbose(path: &str) -> Result<String, io::Error> {
let mut file = match File::open(path) {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut contents = String::new();
match file.read_to_string(&mut contents) {
Ok(_) => Ok(contents),
Err(e) => Err(e),
}
}
fn main() {
match read_file_contents("test.txt") {
Ok(contents) => println!("文件内容:\n{}", contents),
Err(e) => println!("读取失败: {}", e),
}
}
链式 ? 操作
use std::fs::File;
use std::io::{self, Read};
fn read_first_line(path: &str) -> Result<String, io::Error> {
let mut contents = String::new();
File::open(path)?.read_to_string(&mut contents)?;
Ok(contents
.lines()
.next()
.unwrap_or("")
.to_string())
}
fn main() {
match read_first_line("test.txt") {
Ok(line) => println!("第一行: {}", line),
Err(e) => println!("错误: {}", e),
}
}
注意: ? 操作符只能在返回 Result(或 Option)的函数中使用。main 函数也可以返回 Result。
main 函数返回 Result
use std::fs;
use std::io;
fn main() -> Result<(), io::Error> {
let contents = fs::read_to_string("config.txt")?;
println!("配置内容:\n{}", contents);
Ok(())
}
9.4 自定义错误类型
基本自定义错误
use std::fmt;
use std::num::ParseIntError;
#[derive(Debug)]
enum AppError {
NotFound(String),
ParseError(ParseIntError),
ValidationError { field: String, message: String },
IoError(std::io::Error),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::NotFound(item) => write!(f, "未找到: {}", item),
AppError::ParseError(e) => write!(f, "解析错误: {}", e),
AppError::ValidationError { field, message } => {
write!(f, "验证失败 - {}: {}", field, message)
}
AppError::IoError(e) => write!(f, "IO 错误: {}", e),
}
}
}
// 实现 From trait 以支持 ? 自动转换
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self {
AppError::ParseError(e)
}
}
impl From<std::io::Error> for AppError {
fn from(e: std::io::Error) -> Self {
AppError::IoError(e)
}
}
fn parse_age(input: &str) -> Result<u32, AppError> {
let age: u32 = input.parse()?; // 自动调用 From<ParseIntError>
if age > 150 {
Err(AppError::ValidationError {
field: "age".to_string(),
message: "年龄不能超过150岁".to_string(),
})
} else {
Ok(age)
}
}
fn main() {
for input in ["25", "abc", "200"] {
match parse_age(input) {
Ok(age) => println!("年龄: {}", age),
Err(e) => println!("错误: {}", e),
}
}
}
使用 thiserror 简化
# Cargo.toml
[dependencies]
thiserror = "1"
use thiserror::Error;
#[derive(Error, Debug)]
enum AppError {
#[error("未找到: {0}")]
NotFound(String),
#[error("解析错误: {0}")]
ParseError(#[from] std::num::ParseIntError),
#[error("验证失败 - {field}: {message}")]
ValidationError { field: String, message: String },
#[error("IO 错误: {0}")]
IoError(#[from] std::io::Error),
}
fn parse_age(input: &str) -> Result<u32, AppError> {
let age: u32 = input.parse()?;
if age > 150 {
Err(AppError::ValidationError {
field: "age".to_string(),
message: "年龄不能超过150岁".to_string(),
})
} else {
Ok(age)
}
}
fn main() {
for input in ["25", "abc", "200"] {
match parse_age(input) {
Ok(age) => println!("年龄: {}", age),
Err(e) => println!("错误: {}", e),
}
}
}
9.5 anyhow crate
适用于应用程序级别的错误处理:
# Cargo.toml
[dependencies]
anyhow = "1"
use anyhow::{Context, Result, bail, ensure};
use std::fs;
fn read_config() -> Result<String> {
let contents = fs::read_to_string("config.toml")
.context("无法读取配置文件 config.toml")?;
ensure!(!contents.is_empty(), "配置文件为空");
Ok(contents)
}
fn process_config(config: &str) -> Result<u32> {
let port: u32 = config
.trim()
.parse()
.context("配置文件中的端口号无效")?;
if port == 0 {
bail!("端口号不能为0");
}
Ok(port)
}
fn main() -> Result<()> {
match read_config() {
Ok(config) => {
let port = process_config(&config)?;
println!("端口: {}", port);
}
Err(e) => {
// anyhow 的错误包含完整的原因链
println!("错误: {:#}", e);
}
}
Ok(())
}
thiserror vs anyhow
| 特性 | thiserror | anyhow |
|---|
| 定义错误类型 | ✅ 适合 | ❌ 不适合 |
| 库代码 | ✅ 推荐 | ❌ 不推荐 |
| 应用代码 | 可以 | ✅ 推荐 |
| 错误转换 | 手动实现/derive | 自动(Box<dyn Error>) |
| 错误链 | 需手动 | ✅ .context() |
| 类型匹配 | ✅ 可以 match | ❌ 难以 match |
建议: 库使用 thiserror 定义错误类型,应用使用 anyhow 处理错误。
9.6 错误处理最佳实践
分层错误处理
use std::fmt;
// 1. 定义库级别的错误类型
#[derive(Debug)]
enum DatabaseError {
ConnectionFailed(String),
QueryFailed(String),
NotFound,
}
impl fmt::Display for DatabaseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DatabaseError::ConnectionFailed(s) => write!(f, "连接失败: {}", s),
DatabaseError::QueryFailed(s) => write!(f, "查询失败: {}", s),
DatabaseError::NotFound => write!(f, "记录未找到"),
}
}
}
impl std::error::Error for DatabaseError {}
// 2. 服务层使用库错误
#[derive(Debug)]
enum ServiceError {
Database(DatabaseError),
Validation(String),
Unauthorized,
}
impl fmt::Display for ServiceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ServiceError::Database(e) => write!(f, "数据库错误: {}", e),
ServiceError::Validation(s) => write!(f, "验证错误: {}", s),
ServiceError::Unauthorized => write!(f, "未授权"),
}
}
}
impl From<DatabaseError> for ServiceError {
fn from(e: DatabaseError) -> Self {
ServiceError::Database(e)
}
}
// 3. 业务逻辑
fn find_user(id: u32) -> Result<String, ServiceError> {
if id == 0 {
return Err(ServiceError::Validation("无效的用户ID".to_string()));
}
if id > 100 {
return Err(DatabaseError::NotFound.into());
}
Ok(format!("用户{}", id))
}
fn main() {
for id in [0, 1, 200] {
match find_user(id) {
Ok(user) => println!("找到: {}", user),
Err(e) => println!("错误: {}", e),
}
}
}
9.7 业务场景示例
配置文件加载器
use std::collections::HashMap;
use std::fs;
use std::io;
use std::num::ParseIntError;
#[derive(Debug)]
enum ConfigError {
Io(io::Error),
Parse {
line: usize,
message: String,
},
MissingField(String),
}
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConfigError::Io(e) => write!(f, "IO 错误: {}", e),
ConfigError::Parse { line, message } => {
write!(f, "第{}行解析错误: {}", line, message)
}
ConfigError::MissingField(field) => write!(f, "缺少必要字段: {}", field),
}
}
}
impl From<io::Error> for ConfigError {
fn from(e: io::Error) -> Self {
ConfigError::Io(e)
}
}
struct Config {
data: HashMap<String, String>,
}
impl Config {
fn from_file(path: &str) -> Result<Self, ConfigError> {
let contents = fs::read_to_string(path)?;
let mut data = HashMap::new();
for (line_num, line) in contents.lines().enumerate() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let parts: Vec<&str> = line.splitn(2, '=').collect();
if parts.len() != 2 {
return Err(ConfigError::Parse {
line: line_num + 1,
message: format!("无法解析行: {}", line),
});
}
data.insert(
parts[0].trim().to_string(),
parts[1].trim().to_string(),
);
}
Ok(Config { data })
}
fn get(&self, key: &str) -> Option<&String> {
self.data.get(key)
}
fn get_required(&self, key: &str) -> Result<&String, ConfigError> {
self.data
.get(key)
.ok_or_else(|| ConfigError::MissingField(key.to_string()))
}
fn get_or_default<'a>(&'a self, key: &'a str, default: &'a str) -> String {
self.data
.get(key)
.cloned()
.unwrap_or_else(|| default.to_string())
}
}
fn main() {
// 模拟:创建配置文件
let _ = fs::write("app.conf", "# 应用配置\nport=8080\nhost=localhost\ndebug=true\n");
match Config::from_file("app.conf") {
Ok(config) => {
let port = config.get_or_default("port", "3000");
let host = config.get_or_default("host", "127.0.0.1");
println!("服务器: {}:{}", host, port);
match config.get_required("debug") {
Ok(v) => println!("调试模式: {}", v),
Err(e) => println!("{}", e),
}
}
Err(e) => eprintln!("配置加载失败: {}", e),
}
// 清理
let _ = fs::remove_file("app.conf");
}
9.8 本章小结
| 要点 | 说明 |
|---|
| panic | 不可恢复错误,程序终止 |
| Result | 可恢复错误,必须处理 |
| unwrap/expect | 快捷方式,失败时 panic |
| ? 操作符 | 传播错误,简化代码 |
| 自定义错误 | 实现 Display 和 Error trait |
| thiserror | 简化自定义错误定义 |
| anyhow | 应用级错误处理,带错误链 |
| 分层处理 | 库定义错误类型,应用统一处理 |
扩展阅读
- Rust Book - 错误处理 — 官方教程
- thiserror 文档 — derive 宏文档
- anyhow 文档 — 应用级错误处理
- Rust 错误处理最佳实践 — Andrew Gallant 博文