Rust 错误处理的“左右手”:anyhow 和 thiserror
今天咱们来唠唠 Rust 开发中两个超实用的错误处理库——anyhow和thiserror。要是你已经在 Rust 的世界里摸爬滚打了一段时间,那这两个名字肯定不陌生。它们就像是 Rust 错误处理的“左右手”,各有各的厉害,各有各的用处。接下来,我就带大家好好了解一下它们,顺便比比谁更适合你手头的项目。
1. 认识 anyhow:简单粗暴的“救火队长”
先说说anyhow吧。这哥们儿简直就是个“救火队长”,遇到错误,不管三七二十一,先给你扑灭再说。它的核心思想特别简单:用一个通用的错误类型anyhow::Error,把各种各样的错误都装进去。这样一来,你再也不用操心那些复杂的错误类型定义了,直接甩一个anyhow!宏,问题搞定!
anyhow具备以下几个关键特性:
- 良好的易用性:
anyhow提供了一个通用的错误类型anyhow::Error,这个类型可以包含任何实现了std::error::Error的错误。这意味着你可以使用anyhow::Error来包装几乎所有类型的错误,无需担心具体的错误类型。 - 简洁的错误传播:
anyhow支持通过?操作符来传播错误,同时保留错误发生的上下文。这让错误处理更加直观,同时还能保留错误链,便于调试。 - 方便的开发调试:
anyhow支持通过{:#}格式化指示符来打印错误及其所有相关的上下文和原因,这使得调试复杂的错误链变得更加简单。 - 透明的错误类型: 在开发项目过程中,你只需要将错误信息传递给上一层函数,而不需要关心错误的具体类型。
anyhow让这一过程变得简单,因为它可以包装任何错误,而不需要显式地指定错误类型。
使用 anyhow 的典型场景包括快速原型开发、应用程序顶层的错误处理,或者在库中作为返回错误类型的一个简便选择,尤其是你开发的crate被第三方使用时,第三方可以不需要关心具体的错误类型。一般来说,anyhow可通过如下几种方式提供错误处理:
- 使用
Result<T, anyhow::Error>或者anyhow::Result<T>作为返回值,然后利用?语法糖无脑传播报错。 - 使用
with_context(f)或者context(t)来附加错误信息。 - 使用
map_err将标准库的错误转换为anyhow:Error - 使用
anyhow:bail宏提前返回错误,相当于return Err(anyhow!($args...)) - 使用
downcast反解具体的错误类型。
在开发调试过程中,可以使用如下几种方式打印anyhow:
- 可以使用
{}或者.to_string(),但是仅仅打印最外层错误或者上下文,而不是内层的错误; - 可以使用
{:#}打印外层和底层错误; - 可以使用
{:?}在调试模式打印错误以及调用栈; - 可以使用
{:#?}以结构体样式打印错误;
Error {
context: "Failed to read contents from ./path/to/anyhow.txt",
source: Os {
code: 2,
kind: NotFound,
message: "No such file or directory",
},
}
1.1 使用 anyhow!宏
用anyhow可太简单了,就像吃个汉堡一样轻松。首先,得在你的Cargo.toml文件里加上它:
[dependencies]
anyhow = "1.0"
然后,你就可以在代码里尽情发挥啦。比如,直接用anyhow!抛个错误:
use anyhow::{anyhow, Error, Result};
fn use_anyhow() -> Result<()> {
Err(anyhow!("this is error"))
}
使用 anyhow! 这个宏可以生成 anyhow::Error类型的值,它可以接受字符串,格式化字符串作为参数,或者实现 std::error:Error 的错误作为参数。
1.2 使用 ?操作符
anyhow 的一个重要特性是可以与 ? 操作符一起使用,自动将不同的错误类型转换为 anyhow::Error
use std::fs::File;
use anyhow::Result;
fn open_file(path: &str) -> Result<File> {
let file = File::open(path)?;
ok(file)
}
如果 File::open 返回一个错误,? 操作符会自动将其转换为 anyhow::Error,并返回。
1.3 添加上下文信息
有时候,一个简单的错误消息还不够,你可能想加点上下文,让别人更清楚地知道问题出在哪里。anyhow给你准备了context和with_context方法,这就好比在伤口上贴个标签,写上受伤的原因。比如:
use std::fs::File;
use anyhow::{Context, Result};
fn open_file(path: &str) -> Result<File> {
let file = File::open(path).context("Failed to open file")?;
ok(file)
}
fn open_file(path: &str) -> Result<File> {
let file = File::open(path).context(format!("Failed to open file: {}", path))?;
ok(file)
}
fn open_file(path: &str) -> Result<File> {
let file = File::open(path).with_context(|| format!("Failed to open file: {}", path))?;
ok(file)
}
这样,当错误发生的时候,你不仅能知道是文件读取失败了,还能知道是哪个文件出了问题。
1.4 使用错误链
anyhow::Error 支持错误链,即一个错误可以包含另一个错误的信息。这对于调试复杂问题非常有用:
use std::fs::File;
use anyhow::{anyhow, Result};
fn open_file(path: &str) -> Result<File> {
let file = File::open(path).map_err(|e| anyhow!("Failed to open file: {}", e))?;
ok(file)
}
map_err 被用来将标准库的错误转换为 anyhow::Error,并添加额外的上下文信息。
1.5 使用anyhow::bail宏
anyhow::bail宏用于提前错误返回,等价于 return Err(anyhow!($args...))
use anyhow::{bail, Result};
fn open_file(path: &str) -> Result<File> {
let result = File::open(path);
if result.is_err() {
bail!("Failed to open file: {}", result.unwrap_err());
}
ok(result)
}
unwrap_err()函数用于提取result枚举变体Err的错误信息。
1.6 使用downcast反解错误类型
anyhow 提供了 downcast_ref方法,用于在运行时将 anyhow::Error 转换为其包含的具体错误类型的引用。这可以用于检查和处理特定类型的错误。
use anyhow::{Result, Context};
use std::{fs, io};
fn read_and_process_file(file_path: &str) -> Result<()> {
// 尝试读取文件
let data = fs::read_to_string(file_path)
.with_context(||format!("failed to read file `{}`", file_path))?;
// 解析数据
let processed_data = parse_data(&data)
.with_context(||format!("failed to parse data from file `{}`", file_path))?;
// 执行数据操作
perform_some_operation(processed_data)
.with_context(|| "failed to perform operation based on file data")?;
Ok(())
}
fn parse_data(data: &str) -> Result<String> {
Ok(data.to_uppercase())
}
fn perform_some_operation(data: String) -> Result<()> {
println!("processed data: {}", data);
Ok(())
}
fn main() {
let file_path = "./anyhow.txt";
let res = read_and_process_file(file_path);
match res {
Ok(_) => println!("successfully!"),
Err(e) => {
// 使用 downcast 来反解出实际的错误类型,可能出现的异常只能是 io::Error
if let Some(my_error) = e.downcast_ref::<io::Error>() {
println!("has io error: {:#}", my_error);
} else {
println!("unknown error: {:?}", e);
}
}
}
}
2. 认识 thiserror:精雕细琢的“工匠”
说完anyhow,咱们再来看看thiserror。这哥们儿更像是个“工匠”,注重细节,追求完美。它让你自己定义错误类型,每个错误都有自己的名字和描述。虽然用起来比anyhow复杂一点,但好处是,你能更清楚地知道每个错误到底是什么意思。
2.1 怎么用 thiserror
用thiserror,首先得在Cargo.toml里加上它:
[dependencies]
thiserror = "2.0"
2.0以上版本要求编译器 rustc 1.61+, 然后,你就可以开始定义自己的错误类型了:
use std::io;
use thiserror::Error;
#[derive(Error,Debug)]
pub enum DataStoreError {
#[error("data store disconnected")]
Disconnect(#[from] std::io::Error),
#[error("the data for key `{0}` is not available")]
Redaction(String),
#[error("invalid header (expected {expected:?},found {found:?})")]
InvalidHeader {expected:String,found:String},
#[error("unknown data store error")]
Unknown
}
fn main() {
println!("Unknown {}", DataStoreError::Unknown);
println!("InvalidHeader {}", DataStoreError::InvalidHeader {
expected : String::from("expected"),
found : String::from("found")
});
println!("Redaction {}",DataStoreError::Redaction(String::from("Redaction")));
println!("Disconnect {}",DataStoreError::Disconnect(io::Error::from(io::ErrorKind::TimedOut)));
}
看,每个错误都有自己的名字和描述,清清楚楚。要是你在函数里遇到错误,直接用?,thiserror会帮你把错误类型转换好,就像一个贴心的助手。
2.2 使用#[error]
如果使用 #[error(...)] 为枚举或者结构体生成自定义错误消息,这将为它们实现 Display:
#[error("{var}")]⟶write!("{}", self.var)#[error("{0}")]⟶write!("{}", self.0)#[error("{var:?}")]⟶write!("{:?}", self.var)#[error("{0:?}")]⟶write!("{:?}", self.0)
2.2.1 枚举
use thiserror::Error;
#[derive(Debug)]
pub struct Limits{
lo : i16,
hi : i16
}
#[derive(Error,Debug)]
pub enum MyError{
#[error("invalid rdo_lookahead_frames {0} (expected < {max})", max = i32::MAX)]
InvalidLookahead(u32),
#[error("first letter must be lowercase but was {:?}", first_char(.0).unwrap_or(char::default()))]
WrongCase(String),
#[error("invalid index {idx},expected at least {} and at most {}", .limits.lo, .limits.hi)]
OutOfBounds{idx:usize, limits:Limits}
}
fn first_char(s: &str) -> Option<char> {
let first = s.chars().next()?;
Some(first)
}
fn main() {
println!("InvalidLookahead {}",MyError::InvalidLookahead(3333));
println!("WrongCase {}",MyError::WrongCase("kk".to_string()));
println!("OutOfBounds {}",MyError::OutOfBounds{idx : 89,limits:Limits{
lo:12,
hi:11
}});
}
2.2.2 结构体
use thiserror::Error;
#[derive(Error, Debug)]
#[error("username {username}(money:{money})")]
struct MyErrorStruct {
username: String,
money: u32,
}
fn check_money(money: u32) -> Result<(), MyErrorStruct> {
if money < 100 {
Err(MyErrorStruct {
username: "jack.ma".into(),
money,
})
} else {
Ok(())
}
}
fn main() {
if let Err(e) = check_money(99) {
println!("check money: {}", e);
}
}
2.3 使用#[from]
错误类型转换:#[error(transparent)]属性意味着该错误只是作为其他错误的容器,它的错误消息将直接从其“源”错误中继承。
#[from]属性标记意味着io::Error可以自动转换为MyError::IoError。
use std::io;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum MyError {
#[error(transparent)]
IOError(#[from] io::Error),
}
fn main() {
println!("MyError IOError: {}", MyError::IOError(io::Error::from(io::ErrorKind::TimedOut)));
if let Err(e) = std::fs::File::open("thiserror.txt") {
println!("open file: {}", e);
}
}
嵌套错误:错误链允许捕获并响应从底层库或函数传播出来的错误。
#[derive(Error, Debug)]
pub enum MyError {
#[error("io error occurred")]
IOError(#[from] io::Error),
}
fn main() {
if let Err(e) = std::fs::read_to_string("thiserror.txt").map_err(MyError::from) {
println!("{:#?}", e);
}
}
2.4 使用#[source]
使字段命名为 source,可为自定义错误实现 source 方法,返回底层的错误类型:
use std::io;
use thiserror::Error;
use std::error::Error;
#[derive(Error, Debug)]
pub enum MyError {
#[error("some io error happened, {:?}", .source)]
IOError { source: io::Error },
}
fn main() {
let err = MyError::IOError {
source: io::Error::from(io::ErrorKind::TimedOut),
};
println!("{:?}", err.source());
}
使用 #[source] 属性标记非 source 的字段:
#[derive(Error, Debug)]
pub enum MyError {
#[error("some io error happened, {:?}", .err)]
IO {
#[source]
err: io::Error,
},
}
fn main() {
let err = MyError::IO {
err: io::Error::from(io::ErrorKind::TimedOut),
};
println!("{:?}", err.source());
}
#[from] 和 #[source] 二选一即可,#[from] 也会为类型生成 .source() 方法:
use std::error::Error as _;
use std::io;
use thiserror::Error;
#[derive(Error, Debug)]
#[error("some io error happened, {:?}", .source)]
pub struct MyError {
#[from]
source: io::Error,
}
fn main() {
let err = MyError::from(io::Error::from(io::ErrorKind::TimedOut));
println!("{:?}", err.source());
}
3. anyhow 和 thiserror 的“擂台赛”
好啦,现在咱们已经认识了anyhow和thiserror,接下来咱们来比比它们,看看谁更适合你。
3.1 错误类型:简单 vs 精细
anyhow是个“大杂烩”,不管什么错误,统统装进anyhow::Error里。好处是简单,坏处是不够精细。thiserror则是个“细节控”,让你自己定义错误类型,每个错误都有自己的名字和描述。好处是清晰,坏处是稍微复杂一点。
3.2 上下文信息:自动 vs 手动
anyhow的上下文信息是自动记录的,你啥都不用管,它自己就帮你搞定。thiserror则需要你手动定义上下文信息,虽然麻烦一点,但能让你更清楚地表达错误的含义。
3.3 使用场景:快速开发 vs 精准控制
如果你只是想快速开发一个功能,不想被复杂的错误类型定义困扰,anyhow绝对是你的不二之选。它就像一把瑞士军刀,虽然功能简单,但足够应对大部分情况。如果你的项目对错误处理要求特别高,需要精确控制每个错误的类型和上下文,那thiserror就是你的“秘密武器”。
3.4 anyhow和thiserror配合使用
通常thiserror用于对外封装的库crate实现,提供对外的精确错误控制类型,而anyhow则更方便的用于内部逻辑实现。
use thiserror::Error;
use std::fs::{self, File};
use anyhow::{Context, Result};
fn open_file(path: &str) -> Result<File> {
let file = File::open(path).context(format!("Failed to open file: {}", path))?;
Ok(file)
}
fn read_file(path: &str) -> Result<String> {
let data = fs::read_to_string(path).context(format!("Failed to read file: {}", path))?;
Ok(data)
}
#[derive(Error, Debug)]
pub enum AnyOpenError {
#[error(transparent)]
OpenFileError(#[from] anyhow::Error),
}
#[derive(Error, Debug)]
pub enum AnyReadError {
#[error(transparent)]
ReadFileError(#[from] anyhow::Error),
}
#[derive(Error, Debug)]
pub enum MyError {
#[error(transparent)]
OpenError(AnyOpenError),
#[error(transparent)]
ReadError(AnyReadError),
}
fn process(path: &str) -> core::result::Result<(), MyError> {
if let Err(e) = open_file(path) {
return Err(MyError::OpenError(AnyOpenError::from(e)))
}
if let Err(e) = read_file(path) {
return Err(MyError::ReadError(AnyReadError::from(e)))
}
Ok(())
}
fn main() {
if let Err(e) = process("thiserror.txt") {
println!("{:#?}", e);
}
}
4.写在最后
好了,朋友们,今天就聊到这里。anyhow和thiserror都是 Rust 错误处理的利器,各有各的用处。anyhow像个“救火队长”,简单直接,适合快速开发;thiserror像个“工匠”,注重细节,适合需要精准控制的场景。希望这篇文章能帮你在开发中更好地选择合适的工具,让代码更优雅,让生活更美好!
