跳到主要内容

错误处理

错误处理是编写健壮程序的关键。Rust 将错误分为两类:可恢复错误和不可恢复错误,并提供了不同的处理机制。这种设计理念让 Rust 程序在保证安全性的同时,提供了清晰的错误处理路径。

为什么要学习错误处理?

在程序运行过程中,各种意外情况随时可能发生:文件可能不存在、网络可能中断、用户可能输入无效数据。如何优雅地处理这些情况,直接决定了程序的稳定性和用户体验。

Rust 的错误处理有以下特点:

  • 编译时检查:编译器强制你处理可能的错误,避免运行时意外崩溃
  • 类型安全:错误信息通过类型系统传递,不会丢失
  • 零成本抽象:错误处理不会带来运行时性能开销
  • 清晰的意图:函数签名明确表明是否会返回错误

错误分类

Rust 将错误分为两大类:

错误类型说明处理方式典型场景
可恢复错误可以尝试恢复或报告用户Result<T, E>文件不存在、网络超时
不可恢复错误程序 bug,无法继续执行panic!数组越界、除以零

核心原则

  • 如果错误是预期的、可以处理的,使用 Result
  • 如果错误表示程序状态不一致、是 bug,使用 panic!

panic! 不可恢复错误

当程序遇到无法处理的错误时,可以使用 panic! 宏让程序立即终止。这是 Rust 处理不可恢复错误的方式。

直接调用 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,这些通常是程序 bug 的表现:

fn main() {
// 数组越界访问
let v = vec![1, 2, 3];
// let element = v[10]; // panic: index out of bounds: the len is 3 but the index is 10

// 整数溢出(仅在 debug 模式下 panic)
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 发生时的完整调用链,类似于:

thread 'main' panicked at 'index out of bounds', src/main.rs:4:5
stack backtrace:
0: rust_begin_unwind
at /rustc/.../library/std/src/panicking.rs:578:5
1: core::panicking::panic_fmt
at /rustc/.../library/core/src/panicking.rs:67:14
2: core::panicking::panic_bounds_check
at /rustc/.../library/core/src/panicking.rs:162:5
3: main::main
at ./src/main.rs:4:5

何时使用 panic!

// 示例:原型代码中可以使用 unwrap 快速验证想法
fn prototype_example() {
// 在原型开发阶段,可以使用 unwrap 简化代码
let home: String = std::env::var("HOME").unwrap();
println!("Home: {}", home);
}

// 示例:测试代码中可以使用 unwrap
#[cfg(test)]
mod tests {
#[test]
fn test_example() {
let result = some_function().unwrap();
assert_eq!(result, 42);
}
}

// 示例:确定值一定存在时使用 unwrap
fn example() {
let s = String::from("hello");
// 我们确定这是一个有效的 UTF-8 字符串
let bytes = s.as_bytes();
// 可以安全地使用 unwrap
let first_byte = bytes.first().unwrap();
}

适合使用 panic! 的场景

  1. 原型代码:快速验证想法,后续再添加错误处理
  2. 测试代码:测试期望某些操作成功
  3. 程序状态不一致:无法继续执行时
  4. 你比编译器更确定:确定某个操作不会失败

Result 可恢复错误

Result<T, E> 是 Rust 处理可恢复错误的核心类型。它是一个枚举,表示操作可能成功或失败。

Result 类型定义

enum Result<T, E> {
Ok(T), // 成功,包含结果值
Err(E), // 失败,包含错误信息
}

T 是成功时返回的值的类型,E 是失败时返回的错误类型。

基本使用

use std::fs::File;
use std::io::ErrorKind;

fn main() {
let f = File::open("hello.txt");

let f = match f {
Ok(file) => {
println!("文件打开成功");
file
}
Err(error) => {
println!("文件打开失败: {}", error);
return;
}
};

// 使用文件...
}

这段代码尝试打开一个文件,如果成功则继续使用,如果失败则打印错误信息并返回。

匹配不同的错误类型

当需要根据不同类型的错误采取不同行动时,可以嵌套 match:

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 => match File::create("hello.txt") {
Ok(fc) => {
println!("文件创建成功");
fc
}
Err(e) => {
panic!("创建文件时出错: {:?}", e);
}
},
// 权限不足
ErrorKind::PermissionDenied => {
panic!("没有权限访问文件");
}
// 其他错误
other_error => {
panic!("打开文件时出错: {:?}", other_error);
}
},
};
}

为什么要区分错误类型?

不同的错误可能需要不同的处理策略。例如,文件不存在时可以尝试创建,但权限不足时需要提示用户。这种细粒度的错误处理让程序更加智能。

unwrap 和 expect

unwrapexpect 是简化 Result 处理的便捷方法:

use std::fs::File;

fn main() {
// unwrap:成功返回值,失败 panic
// 不推荐在生产代码中使用
let f1 = File::open("hello.txt").unwrap();

// expect:可以自定义错误信息
// 比 unwrap 更好,因为提供了上下文
let f2 = File::open("hello.txt").expect("无法打开 hello.txt");
}

unwrap vs expect 的区别

use std::fs::File;

fn main() {
// unwrap 的错误信息不明确
// thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: ...'
let f1 = File::open("missing.txt").unwrap();

// expect 提供了明确的错误信息
// thread 'main' panicked at '配置文件不存在,请检查路径'
let f2 = File::open("config.txt").expect("配置文件不存在,请检查路径");
}

建议:在生产代码中,至少使用 expect 而不是 unwrap,以便在出错时提供有意义的信息。

? 运算符

? 运算符是 Rust 中简化错误传播的语法糖,它让错误处理代码更加简洁。

? 运算符的工作原理

当在 Result 上使用 ? 时:

  • 如果 ResultOk,取出其中的值继续执行
  • 如果 ResultErr,立即将错误返回给调用者

传统方式 vs ? 运算符

use std::fs::File;
use std::io::{self, Read};

// 传统方式:使用 match 处理每个可能的错误
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),
}
}

? 运算符的链式调用

? 运算符可以链式使用,让代码更加紧凑:

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];

// 使用 ? 运算符:如果返回 None,函数立即返回 None
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,
kind: MyErrorKind,
}

#[derive(Debug)]
enum MyErrorKind {
NotFound,
InvalidInput,
Internal,
}

impl MyError {
fn new(msg: &str, kind: MyErrorKind) -> MyError {
MyError {
message: msg.to_string(),
kind,
}
}
}

// 实现 Display trait,用于格式化输出
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}: {}", self.kind, self.message)
}
}

// 实现 Error trait,使其成为标准错误类型
impl Error for MyError {}

fn divide(a: i32, b: i32) -> Result<i32, MyError> {
if b == 0 {
return Err(MyError::new("除数不能为零", MyErrorKind::InvalidInput));
}
Ok(a / b)
}

fn main() {
match divide(10, 0) {
Ok(result) => println!("结果: {}", result),
Err(e) => println!("错误: {}", e),
}
}

使用 thiserror 简化错误定义

thiserror 是一个流行的第三方库,可以大大简化自定义错误类型的定义:

// Cargo.toml:
// [dependencies]
// thiserror = "1.0"

use thiserror::Error;

#[derive(Error, Debug)]
enum AppError {
#[error("IO 错误: {0}")]
Io(#[from] std::io::Error), // 自动实现 From<std::io::Error>

#[error("解析错误: {0}")]
Parse(#[from] std::num::ParseIntError), // 自动实现 From<ParseIntError>

#[error("用户 {name} 不存在")]
UserNotFound { name: String },

#[error("权限不足")]
PermissionDenied,
}

// 使用示例
fn read_and_parse() -> Result<i32, AppError> {
let content = std::fs::read_to_string("number.txt")?; // io::Error 自动转换为 AppError
let number: i32 = content.trim().parse()?; // ParseIntError 自动转换为 AppError
Ok(number)
}

fn main() {
match read_and_parse() {
Ok(n) => println!("数字: {}", n),
Err(e) => println!("错误: {}", e),
}
}

使用 Box<dyn Error> 处理多种错误

当函数可能返回多种不同类型的错误时,可以使用 Box<dyn Error>

use std::fs::File;
use std::io::{self, Read};
use std::num::ParseIntError;

// Box<dyn Error> 可以容纳任何实现了 Error trait 的类型
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),
}
}

什么时候使用 Box<dyn Error>

  • 快速原型开发
  • 主函数(main)的返回类型
  • 不需要对错误进行细分处理的场景

什么时候定义自定义错误类型?

  • 需要对不同错误进行不同处理
  • 需要向用户显示特定的错误信息
  • 生产环境的库或核心模块

Option vs Result

OptionResult 都用于表示可能失败的操作,但使用场景不同:

fn main() {
// Option:值可能不存在
let maybe_number: Option<i32> = Some(42);

// Result:操作可能失败,带有错误原因
let result: Result<i32, String> = Ok(42);

// Option 转 Result
let ok_option: Result<i32, &str> = Some(42).ok_or("没有值");

// Result 转 Option
let err_result: Result<i32, &str> = Err("错误");
let none_option: Option<i32> = err_result.ok(); // None
}

选择指南

场景使用类型说明
值可能不存在Option<T>如查找操作、可能为空的字段
操作可能失败且有原因Result<T, E>如 I/O 操作、解析操作
简单查找操作Option<T>如 HashMap 的 get 方法
需要错误传播Result<T, E>如文件读取、网络请求
需要链式操作两者都支持使用 ? 运算符

Option 的常用方法

fn main() {
let some_value: Option<i32> = Some(42);
let none_value: Option<i32> = None;

// unwrap_or:提供默认值
let value = none_value.unwrap_or(0); // 0

// unwrap_or_else:延迟计算默认值
let value = none_value.unwrap_or_else(|| {
println!("计算默认值");
0
});

// map:转换值
let doubled = some_value.map(|x| x * 2); // Some(84)

// and_then:链式操作
let result = some_value
.and_then(|x| if x > 0 { Some(x) } else { None })
.map(|x| x * 2); // Some(84)

// filter:过滤值
let filtered = some_value.filter(|&x| x > 100); // None
}

错误处理最佳实践

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").unwrap();

// 推荐:提供上下文信息
let f = File::open("config.txt")
.expect("无法打开配置文件,请确保文件存在");
}

3. 使用自定义错误类型

use std::fmt;

#[derive(Debug)]
enum AppError {
ConfigMissing,
InvalidFormat(String),
DatabaseError(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),
AppError::DatabaseError(msg) => write!(f, "数据库错误: {}", msg),
}
}
}

impl std::error::Error for AppError {}

fn load_config() -> Result<String, AppError> {
// 模拟加载配置
Err(AppError::ConfigMissing)
}

4. 使用 anyhow 和 thiserror(生产环境推荐)

// anyhow:简化应用程序的错误处理
// 适合用于应用程序(application)
use anyhow::{Context, Result};

fn read_config() -> Result<String> {
std::fs::read_to_string("config.txt")
.context("无法读取配置文件")
}

// thiserror:简化库的错误定义
// 适合用于库(library)
use thiserror::Error;

#[derive(Error, Debug)]
#[error("配置错误: {message}")]
struct ConfigError {
message: String,
}

5. 不要忽略错误

fn main() {
// 错误:忽略错误,可能导致隐藏的 bug
// let _ = File::open("config.txt");

// 正确:至少记录错误
if let Err(e) = std::fs::read_to_string("config.txt") {
eprintln!("警告: 无法读取配置文件: {}", e);
}
}

6. 为 main 函数使用 Result

use std::fs;

// main 函数可以返回 Result
fn main() -> Result<(), Box<dyn std::error::Error>> {
let content = fs::read_to_string("config.txt")?;
println!("配置内容: {}", content);
Ok(())
}

常见错误处理模式

提供默认值

fn main() {
// 使用 unwrap_or 提供默认值
let value: i32 = "abc".parse().unwrap_or(0);
println!("值: {}", value); // 值: 0

// 使用 unwrap_or_else 延迟计算
let value: i32 = "abc".parse().unwrap_or_else(|_| {
println!("解析失败,使用默认值");
0
});

// 使用 map 和 unwrap_or 组合
let config = std::env::var("CONFIG_PATH")
.ok() // Result -> Option
.map(|p| format!("{}/config.json", p))
.unwrap_or_else(|| "default_config.json".to_string());
}

重试机制

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 * retries as u64));
}
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;
use std::num::ParseIntError;

#[derive(Debug)]
enum AppError {
IoError(std::io::Error),
ParseError(ParseIntError),
}

// 实现 From trait 进行错误转换
impl From<std::io::Error> for AppError {
fn from(err: std::io::Error) -> AppError {
AppError::IoError(err)
}
}

impl From<ParseIntError> for AppError {
fn from(err: ParseIntError) -> AppError {
AppError::ParseError(err)
}
}

fn read_number() -> Result<i32, AppError> {
// ? 自动调用 From trait 进行转换
let content = fs::read_to_string("number.txt")?; // io::Error -> AppError
let number = content.trim().parse()?; // ParseIntError -> AppError
Ok(number)
}

小结

本章我们学习了:

  1. 错误分类:可恢复错误(Result)和不可恢复错误(panic!)
  2. panic!:处理不可恢复错误,程序立即终止
  3. Result<T, E>:处理可恢复错误,可以尝试恢复或报告
  4. ? 运算符:简化错误传播,让代码更简洁
  5. 自定义错误类型:使用 thiserror 简化定义
  6. Option vs Result:选择合适的类型处理可能缺失的值
  7. 最佳实践:提供有意义的错误信息,不要忽略错误

练习

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

参考资料