跳到主要内容

错误处理

错误处理是编写健壮程序的关键。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

unwrapexpect 是简化 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");
}

注意:生产代码中应谨慎使用 unwrapexpect,它们会在错误时 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),
}
}

? 运算符的工作原理

  • 如果 ResultOk,返回其中的值
  • 如果 ResultErr,立即返回错误

? 运算符的链式调用

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);
// 可以选择使用默认配置继续运行
}
}
}

小结

本章我们学习了:

  1. 错误分类:可恢复错误和不可恢复错误
  2. panic!:处理不可恢复错误
  3. Result<T, E>:处理可恢复错误
  4. ? 运算符:简化错误传播
  5. Option vs Result:选择合适的类型
  6. 最佳实践:提供有意义的错误信息、使用自定义错误类型

练习

  1. 编写一个函数,打开文件并读取内容,使用 ? 运算符处理错误
  2. 定义一个自定义错误类型,表示不同的用户输入错误
  3. 实现一个重试机制,在网络请求失败时自动重试
  4. 编写一个函数,将字符串解析为整数,处理可能的错误

参考资源