跳到主要内容

错误处理

问题

Rust 的错误处理机制是什么?为什么没有 try/catch?

答案

Rust 将错误分为两类,用不同机制处理:

错误类型机制说明
不可恢复错误panic!程序 bug,直接崩溃(如数组越界、unwrap 失败)
可恢复错误Result<T, E>预期中的失败(如文件不存在、网络超时)

panic! — 不可恢复错误

fn main() {
// 显式 panic
panic!("崩溃了!");

// 隐式 panic
let v = vec![1, 2, 3];
v[99]; // index out of bounds → panic
}
panic 的行为
  • 默认(unwinding):展开调用栈,逐层执行析构函数清理资源
  • abort 模式:直接终止进程,不清理。通过 Cargo.toml 设置:
Cargo.toml
[profile.release]
panic = 'abort' # 减小二进制大小

Result — 可恢复错误

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

fn read_file(path: &str) -> Result<String, io::Error> {
let mut file = File::open(path)?; // 失败则提前返回 Err
let mut content = String::new();
file.read_to_string(&mut content)?;
Ok(content)
}

fn main() {
match read_file("config.toml") {
Ok(content) => println!("{}", content),
Err(e) => eprintln!("读取失败: {}", e),
}
}

? 操作符

? 是错误传播的语法糖,等价于 match + early return:

// 使用 ?
fn read_username() -> Result<String, io::Error> {
let content = std::fs::read_to_string("username.txt")?;
Ok(content.trim().to_string())
}

// 等价的 match 写法
fn read_username_verbose() -> Result<String, io::Error> {
let content = match std::fs::read_to_string("username.txt") {
Ok(c) => c,
Err(e) => return Err(e),
};
Ok(content.trim().to_string())
}

? 还可以链式调用:

fn read_username() -> Result<String, io::Error> {
let mut s = String::new();
File::open("username.txt")?.read_to_string(&mut s)?;
Ok(s.trim().to_string())
}
? 的适用条件
  1. ? 只能用在返回 ResultOption 的函数中
  2. ? 会自动调用 From::from() 转换错误类型(如果目标 Err 类型实现了 From<源错误>
  3. main 函数中使用 ? 需要改签名:fn main() -> Result<(), Box<dyn Error>>

自定义错误类型

use std::fmt;
use std::num::ParseIntError;
use std::io;

#[derive(Debug)]
enum AppError {
Io(io::Error),
Parse(ParseIntError),
Custom(String),
}

// 实现 Display(给用户看的信息)
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::Custom(msg) => write!(f, "{}", msg),
}
}
}

// 实现 Error trait
impl std::error::Error for AppError {}

// 实现 From,让 ? 自动转换
impl From<io::Error> for AppError {
fn from(e: io::Error) -> Self { AppError::Io(e) }
}

impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self { AppError::Parse(e) }
}

fn process() -> Result<i32, AppError> {
let content = std::fs::read_to_string("number.txt")?; // io::Error → AppError
let num: i32 = content.trim().parse()?; // ParseIntError → AppError
Ok(num * 2)
}

thiserror — 简化自定义错误

thiserror 用 derive 宏自动生成错误类型的样板代码:

use thiserror::Error;

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

#[error("解析错误: {0}")]
Parse(#[from] std::num::ParseIntError),

#[error("自定义错误: {0}")]
Custom(String),

#[error("验证失败: {field} 的值 {value} 不合法")]
Validation { field: String, value: String },
}

anyhow — 快速错误处理

anyhow 提供动态错误类型,适合应用层快速处理:

use anyhow::{Context, Result, bail, ensure};

fn process_config() -> Result<Config> {
let content = std::fs::read_to_string("config.toml")
.context("读取配置文件失败")?; // 附加上下文信息

let config: Config = toml::from_str(&content)
.context("解析配置文件失败")?;

ensure!(config.port > 0, "端口号必须大于 0"); // 条件不满足则返回 Err

if config.name.is_empty() {
bail!("名称不能为空"); // 直接返回 Err
}

Ok(config)
}

thiserror vs anyhow

方面thiserroranyhow
用途库开发应用开发
错误类型自定义枚举(精确匹配)anyhow::Error(动态)
调用方处理可以 match 不同错误只能看错误信息链
上下文手动.context() 自动附加
依赖编译期(proc-macro)运行时(动态分发)
最佳实践
  • 写库thiserror:调用方需要精确匹配不同错误类型
  • 写应用anyhow:快速传播错误,不需要精确分类
  • 两者可以组合:库用 thiserror 定义错误,应用用 anyhow 包装

Option 的错误处理

? 同样适用于 Option

fn first_even(numbers: &[i32]) -> Option<i32> {
let first = numbers.first()?; // None 则提前返回 None
if first % 2 == 0 {
Some(*first)
} else {
None
}
}

Option 转 Result:

let value: Option<i32> = Some(42);

// Option → Result
let result: Result<i32, &str> = value.ok_or("值不存在");
let result: Result<i32, String> = value.ok_or_else(|| format!("值不存在"));

// Result → Option
let opt: Option<i32> = "42".parse::<i32>().ok();

常见面试问题

Q1: Rust 为什么不用 try/catch 异常机制?

答案

  1. 显式性Result<T, E> 在函数签名中明确表达"这个函数可能失败",调用方无法忽略
  2. 零成本Result 是普通枚举,没有异常展开的运行时开销
  3. 类型安全:编译器强制你处理错误,不会像异常一样被遗漏
  4. 可组合? 让错误传播几乎和异常一样简洁,但不失显式性

异常机制的问题:函数签名看不出会抛什么异常(Java 的 checked exception 尝试解决但效果不佳),异常展开有运行时开销。

Q2: unwrap()expect() 应该在什么场景使用?

答案

两者在 None/Err 时都会 panic,区别是 expect 可以附加自定义消息。

适用场景

  • 测试代码:测试中可以自由 unwrap
  • 逻辑上不可能失败:你能证明绝不会是 None/Err(加注释说明原因)
  • 原型开发:快速迭代,之后替换为正确处理
// ✅ 逻辑上不可能失败("10" 一定能解析为 i32)
let n: i32 = "10".parse().expect("硬编码数字一定能解析");

// ❌ 用户输入/外部数据不应 unwrap
let n: i32 = user_input.parse().unwrap(); // 可能 panic

生产代码中应避免 unwrap/expect,用 ? 或 match 正确处理。

Q3: 如何在 main 函数中使用 ?

答案

修改 main 的返回类型:

use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
let content = std::fs::read_to_string("config.toml")?;
println!("{}", content);
Ok(())
}

或使用 anyhow:

use anyhow::Result;

fn main() -> Result<()> {
let content = std::fs::read_to_string("config.toml")?;
println!("{}", content);
Ok(())
}

Q4: Box<dyn Error>anyhow::Error 有什么区别?

答案

方面Box<dyn Error>anyhow::Error
来源标准库第三方 crate
大小16 字节(胖指针)8 字节(瘦指针)
错误链需手动 .source().context() 自动构建
backtrace无内置支持自动捕获(nightly)
降级(downcast)可以可以,更方便

Q5: ? 操作符是如何做错误类型转换的?

答案

? 在传播错误时,会自动调用 From::from() 将源错误类型转换为目标错误类型:

// 函数返回 AppError,但 File::open 返回 io::Error
fn process() -> Result<(), AppError> {
let file = File::open("data.txt")?;
// ? 展开为:
// match File::open("data.txt") {
// Ok(f) => f,
// Err(e) => return Err(AppError::from(e)), // 自动调用 From
// }
Ok(())
}

这就是为什么自定义错误类型需要实现 From<XxxError>(或用 thiserror 的 #[from])。

Q6: panic 和 Result 应该如何选择?

答案

场景选择原因
程序 bug(不应该发生的情况)panic!尽早崩溃,暴露问题
外部输入可能出错Result预期中的失败,需要优雅处理
原型/示例代码unwrap快速开发
库的公共 APIResult让调用方决定如何处理
违反前置条件panic!类似 assert
初始化失败(无法恢复)panic! / process::exit程序无法继续

原则:如果调用方有办法恢复,返回 Result;如果是程序逻辑错误,用 panic!

Q7: 如何为错误添加上下文信息?

答案

// 1. map_err:转换错误
std::fs::read_to_string("config.toml")
.map_err(|e| format!("读取配置失败: {}", e))?;

// 2. anyhow::Context
use anyhow::Context;
std::fs::read_to_string("config.toml")
.context("读取配置文件失败")?;
// 错误信息变为:读取配置文件失败
// Caused by: No such file or directory

// 3. 动态上下文
std::fs::read_to_string(path)
.with_context(|| format!("读取 {} 失败", path))?;

相关链接