错误处理
错误处理是编写健壮程序的关键。Rust 将错误分为两类:可恢复错误和不可恢复错误,并提供了不同的处理机制。
错误分类
Rust 将错误分为两大类:
┌─────────────────────────────────────────────────────────────┐
│ Rust 错误分类 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 可恢复错误 (Recoverable) 不可恢复错误 (Unrecoverable) │
│ ──────────────────────── ──────────────────────────── │
│ 例如:文件不存在 例如:数组越界访问 │
│ 例如:网络连接失败 例如:除以零 │
│ 例如:用户输入错误 例如:访问空指针 │
│ │
│ 处理方式: 处理方式: │
│ Result<T, E> panic! 宏 │
│ 可以尝试恢复或报告错误 程序立即终止 │
│ │
└─────────────────────────────────────────────────────────────┘
panic! 不可恢复错误
当程序遇到无法处理的错误时,可以使用 panic! 宏让程序崩溃。
直接调用 panic!
fn main() {
println!("程序开始");
panic!("程序崩溃了!");
println!("这行不会执行");
}
运行结果:
程序开始
thread 'main' panicked at '程序崩溃了!', src/main.rs:4:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
自动触发 panic
某些操作会自动触发 panic:
fn main() {
// 数组越界
let v = vec![1, 2, 3];
// let element = v[10]; // panic: index out of bounds
// 整数溢出(debug 模式)
let mut x: u8 = 255;
// x += 1; // debug 模式下 panic,release 模式下回绕
// 除以零
// let result = 10 / 0; // panic: attempt to divide by zero
// 访问 Option 的 None
let none: Option<i32> = None;
// let value = none.unwrap(); // panic: called `Option::unwrap()` on a `None` value
}
panic! 的回溯信息
设置 RUST_BACKTRACE 环境变量可以查看详细的调用栈:
# 显示简要回溯
RUST_BACKTRACE=1 cargo run
# 显示完整回溯
RUST_BACKTRACE=full cargo run
何时使用 panic!
// 示例:原型代码中可以使用 panic!
fn prototype_example() {
// 在原型开发阶段,可以用 unwrap 简化代码
let home: String = std::env::var("HOME").unwrap();
println!("Home: {}", home);
}
// 示例:测试代码中可以使用 panic!
#[cfg(test)]
mod tests {
#[test]
fn test_example() {
let result = some_function().unwrap();
assert_eq!(result, 42);
}
}
适合使用 panic! 的场景:
- 原型代码,快速验证想法
- 测试代码
- 程序状态不一致,无法继续执行
- 你比编译器更了解程序状态
Result 可恢复错误
Result<T, E> 是 Rust 处理可恢复错误的核心类型。
Result 类型定义
enum Result<T, E> {
Ok(T), // 成功,包含值
Err(E), // 失败,包含错误
}
基本使用
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => {
println!("文件打开成功");
file
}
Err(error) => {
println!("文件打开失败: {}", error);
return;
}
};
// 使用文件...
}
匹配不同的错误
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => {
println!("文件不存在,创建新文件");
File::create("hello.txt").unwrap()
}
ErrorKind::PermissionDenied => {
panic!("没有权限访问文件");
}
_ => {
panic!("打开文件时发生错误: {}", error);
}
},
};
}
unwrap 和 expect
unwrap 和 expect 是简化 Result 处理的方法:
use std::fs::File;
fn main() {
// unwrap:成功返回值,失败 panic
let f1 = File::open("hello.txt").unwrap();
// expect:可以自定义错误信息
let f2 = File::open("hello.txt").expect("无法打开 hello.txt");
}
注意:生产代码中应谨慎使用 unwrap 和 expect,它们会在错误时 panic。
? 运算符
? 运算符用于简化错误传播:
use std::fs::File;
use std::io::{self, Read};
// 传统方式
fn read_username_from_file() -> Result<String, io::Error> {
let f = File::open("hello.txt");
let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}
// 使用 ? 运算符简化
fn read_username_simple() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
// 更简洁的写法
fn read_username_short() -> Result<String, io::Error> {
std::fs::read_to_string("hello.txt")
}
fn main() {
match read_username_simple() {
Ok(content) => println!("内容: {}", content),
Err(e) => println!("错误: {}", e),
}
}
? 运算符的工作原理:
- 如果
Result是Ok,返回其中的值 - 如果
Result是Err,立即返回错误
? 运算符的链式调用
use std::fs::File;
use std::io::{self, Read};
fn read_file_content() -> Result<String, io::Error> {
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}
fn main() {
match read_file_content() {
Ok(content) => println!("{}", content),
Err(e) => eprintln!("错误: {}", e),
}
}
? 运算符与 Option
? 运算符也可以用于 Option:
fn find_first_double_digit(numbers: &[i32]) -> Option<i32> {
for &n in numbers {
if n > 9 && n < 100 {
return Some(n);
}
}
None
}
fn process() -> Option<i32> {
let numbers = vec![1, 5, 15, 3];
// 使用 ? 运算符
let first = find_first_double_digit(&numbers)?;
Some(first * 2)
}
fn main() {
match process() {
Some(result) => println!("结果: {}", result),
None => println!("没有找到两位数"),
}
}
自定义错误类型
定义错误类型
use std::fmt;
use std::error::Error;
#[derive(Debug)]
struct MyError {
message: String,
}
impl MyError {
fn new(msg: &str) -> MyError {
MyError {
message: msg.to_string(),
}
}
}
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "MyError: {}", self.message)
}
}
impl Error for MyError {}
fn divide(a: i32, b: i32) -> Result<i32, MyError> {
if b == 0 {
return Err(MyError::new("除数不能为零"));
}
Ok(a / b)
}
fn main() {
match divide(10, 0) {
Ok(result) => println!("结果: {}", result),
Err(e) => println!("错误: {}", e),
}
}
使用 thiserror 简化错误定义
// 需要在 Cargo.toml 中添加 thiserror 依赖
// use thiserror::Error;
//
// #[derive(Error, Debug)]
// enum AppError {
// #[error("IO 错误: {0}")]
// Io(#[from] std::io::Error),
//
// #[error("解析错误: {0}")]
// Parse(#[from] std::num::ParseIntError),
//
// #[error("自定义错误: {message}")]
// Custom { message: String },
// }
错误类型转换
use std::fs::File;
use std::io::{self, Read};
use std::num::ParseIntError;
// 使用 Box<dyn Error> 处理多种错误
fn read_and_parse() -> Result<i32, Box<dyn std::error::Error>> {
let mut s = String::new();
File::open("number.txt")?.read_to_string(&mut s)?;
let number: i32 = s.trim().parse()?;
Ok(number)
}
fn main() {
match read_and_parse() {
Ok(n) => println!("数字: {}", n),
Err(e) => println!("错误: {}", e),
}
}
Option vs Result
fn main() {
// Option:可能没有值
let maybe_number: Option<i32> = Some(42);
// Result:可能失败的操作
let result: Result<i32, String> = Ok(42);
// Option 和 Result 的转换
let ok_option = Some(42).ok_or("没有值")?;
let err_result: Result<i32, &str> = Err("错误");
let none_option = err_result.ok();
println!("Option: {:?}", maybe_number);
println!("Result: {:?}", result);
}
选择指南:
| 场景 | 使用类型 |
|---|---|
| 值可能不存在 | Option<T> |
| 操作可能失败且有原因 | Result<T, E> |
| 简单查找操作 | Option<T> |
| I/O 操作 | Result<T, E> |
| 解析操作 | Result<T, E> |
错误处理最佳实践
1. 使用 ? 传播错误
use std::fs::File;
use std::io::{self, Read};
fn read_config() -> Result<String, io::Error> {
let mut content = String::new();
File::open("config.txt")?.read_to_string(&mut content)?;
Ok(content)
}
2. 提供有意义的错误信息
use std::fs::File;
fn main() {
let f = File::open("config.txt")
.expect("无法打开配置文件,请确保文件存在");
}
3. 使用自定义错误类型
use std::fmt;
#[derive(Debug)]
enum AppError {
ConfigMissing,
InvalidFormat(String),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AppError::ConfigMissing => write!(f, "配置文件缺失"),
AppError::InvalidFormat(msg) => write!(f, "格式无效: {}", msg),
}
}
}
fn load_config() -> Result<String, AppError> {
// 模拟加载配置
Err(AppError::ConfigMissing)
}
4. 使用 anyhow 和 thiserror(生产环境推荐)
// anyhow:简化错误处理
// use anyhow::{Context, Result};
//
// fn read_config() -> Result<String> {
// std::fs::read_to_string("config.txt")
// .context("无法读取配置文件")
// }
// thiserror:简化自定义错误
// use thiserror::Error;
//
// #[derive(Error, Debug)]
// #[error("配置错误: {message}")]
// struct ConfigError { message: String }
5. 不要忽略错误
fn main() {
// 错误:忽略错误
// let _ = File::open("config.txt");
// 正确:处理错误
if let Err(e) = std::fs::read_to_string("config.txt") {
eprintln!("警告: 无法读取配置文件: {}", e);
}
}
常见错误处理模式
提供默认值
fn main() {
// 使用 unwrap_or 提供默认值
let value: i32 = "abc".parse().unwrap_or(0);
println!("值: {}", value);
// 使用 unwrap_or_else 延迟计算
let value: i32 = "abc".parse().unwrap_or_else(|_| {
println!("解析失败,使用默认值");
0
});
}
重试机制
use std::fs::File;
use std::io::Read;
use std::thread;
use std::time::Duration;
fn read_with_retry(path: &str, max_retries: u32) -> Result<String, std::io::Error> {
let mut retries = 0;
loop {
match File::open(path) {
Ok(mut file) => {
let mut content = String::new();
file.read_to_string(&mut content)?;
return Ok(content);
}
Err(e) if retries < max_retries => {
retries += 1;
println!("重试 {}/{}", retries, max_retries);
thread::sleep(Duration::from_millis(100));
}
Err(e) => return Err(e),
}
}
}
fn main() {
match read_with_retry("hello.txt", 3) {
Ok(content) => println!("内容: {}", content),
Err(e) => eprintln!("错误: {}", e),
}
}
错误日志记录
use std::fs::File;
use std::io::Read;
fn read_file(path: &str) -> Result<String, std::io::Error> {
let mut file = File::open(path)?;
let mut content = String::new();
file.read_to_string(&mut content)?;
Ok(content)
}
fn main() {
match read_file("config.txt") {
Ok(content) => println!("配置: {}", content),
Err(e) => {
eprintln!("[ERROR] 读取配置失败: {}", e);
// 可以选择使用默认配置继续运行
}
}
}
小结
本章我们学习了:
- 错误分类:可恢复错误和不可恢复错误
- panic!:处理不可恢复错误
- Result<T, E>:处理可恢复错误
- ? 运算符:简化错误传播
- Option vs Result:选择合适的类型
- 最佳实践:提供有意义的错误信息、使用自定义错误类型
练习
- 编写一个函数,打开文件并读取内容,使用
?运算符处理错误 - 定义一个自定义错误类型,表示不同的用户输入错误
- 实现一个重试机制,在网络请求失败时自动重试
- 编写一个函数,将字符串解析为整数,处理可能的错误