
本节系统梳理 Rust 错误处理:从标准 Result<T, E> 与 ? 运算符,到库级自定义错误(thiserror),再到应用层快速落地(anyhow)。目标是:接口清晰、边界合理、信息友好、可追踪可定位。
? 运算符fn read_num(path: &str) -> Result<i32, std::io::Error> {
let s = std::fs::read_to_string(path)?; // `?` 等价于早返回 Err
Ok(s.trim().parse::<i32>().map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?)
}?:若为 Err(e) 则立刻返回 Err(e);若为 Ok(v) 则提取 v。map_err:把下游错误转换为上游需要的错误类型。thiserror库/模块对外公开精确可枚举的错误类型,适合做库边界。
Cargo.toml:
[dependencies]
thiserror = "1"定义:
use thiserror::Error;
#[derive(Debug, Error)]
pub enum RepoError {
#[error("I/O失败: {0}")] Io(#[from] std::io::Error), // From 自动生成
#[error("解析失败: {0}")] ParseInt(#[from] std::num::ParseIntError),
#[error("键不存在: {0}")] MissingKey(String),
}
pub fn load_num(path: &str) -> Result<i32, RepoError> {
let raw = std::fs::read_to_string(path)?; // 自动转成 RepoError::Io
let n: i32 = raw.trim().parse()?; // 自动转成 RepoError::ParseInt
Ok(n)
}要点:
#[from] 派生 From<下游错误>,? 即可自动转换;anyhow适合 CLI/服务应用的顶层错误处理:无需为所有错误建枚举,快速聚合并携带上下文。
Cargo.toml:
[dependencies]
anyhow = "1"使用:
use anyhow::{Context, Result};
fn run(path: &str) -> Result<()> {
let s = std::fs::read_to_string(path)
.with_context(|| format!("读取文件失败: {}", path))?; // 追加上下文
let n: i32 = s.trim().parse()
.context("内容不是有效整数")?; // 简明描述
println!("num={}", n);
Ok(())
}Context/with_context 可级联丰富“错误链”,定位更快。anyhow::Result<T> = Result<T, anyhow::Error>:统一错误盒子,适合应用入口。场景 | 推荐 | 理由 |
|---|---|---|
库(对外 API) | 自定义枚举 + thiserror | 错误可枚举、可匹配、语义稳定 |
应用边界(main/CLI/服务顶层) | anyhow | 快速传递、可堆叠上下文,日志友好 |
库内/应用内子模块 | Result<T, E> + ? | 清晰、无样板,按需 map_err |
anyhow::Error 统一日志;impl From<MyErr> for anyhow::Error(anyhow已内置大部分 From)。fn service_handle(path: &str) -> anyhow::Result<()> {
let n = load_num(path)?; // RepoError 自动转 anyhow::Error
println!("n={}", n);
Ok(())
}fn parse_list(s: &str) -> Result<Vec<i32>, std::num::ParseIntError> {
s.split(',').map(|t| t.trim().parse()).collect()
}
fn io_then_parse(path: &str) -> anyhow::Result<Vec<i32>> {
let raw = std::fs::read_to_string(path)?;
Ok(parse_list(&raw)?)
}#[test]
fn test_parse_err() {
let err = "a,b".trim().parse::<i32>().unwrap_err();
assert_eq!(err.kind(), std::num::IntErrorKind::InvalidDigit);
}Drop 中 panic! → 可能触发二次 panic;改为日志记录。unwrap()/expect() → 生产环境尽量改为 ? 或显式处理。anyhow 做库错误 → 失去可匹配性与稳定 API;库层用 thiserror。with_context() 堆叠关键信息:参数、路径、远端地址等。#[from] 与 source。先创建个文件,看好项目根目录。

use anyhow::{Context, Result};
use std::io::{Read, Write};
fn copy_file(from: &str, to: &str) -> Result<()> {
// 打开源文件,添加上下文信息
let mut input = std::fs::File::open(from)
.with_context(|| format!("打开源文件失败: {}", from))?;
// 创建目标文件,添加上下文信息
let mut output = std::fs::File::create(to)
.with_context(|| format!("创建目标文件失败: {}", to))?;
// 读取源文件内容到缓冲区
let mut buf = Vec::new();
input.read_to_end(&mut buf)
.context("读取源文件内容失败")?;
// 将缓冲区内容写入目标文件
output.write_all(&buf)
.context("写入目标文件内容失败")?;
Ok(())
}
// 测试函数(可选,用于验证功能)
fn main() -> Result<()> {
// 示例:复制当前目录下的 "source.txt" 到 "dest.txt"
copy_file("source.txt", "dest.txt")
.with_context(|| "文件复制过程出错")?;
println!("文件复制成功!");
Ok(())
}
thiserror::Error,实现 From。sum <path>:逐行读取整数求和,用 anyhow::Context 给每一步加来源信息。anyhow::Error 暴露?如何在不破坏稳定 API 的前提下追加新错误场景?小结:Rust 错误处理的精髓在于“分层设计”:库层自定义可匹配错误,应用层聚合并堆叠上下文;? 串联流程,thiserror/anyhow 减少样板代码,让错误既易查也易用。