Rust 错误处理的黄金搭档(thiserror和anyhow)
在 Rust 开发中,错误处理是不可或缺的核心环节,但手动实现错误相关 trait 往往会产生大量的样板代码。而为了解决这些样板代码,那就不得不提到两个主流的 Rust 错误处理库 thiserror 和 anyhow 了,现在,我们一起来看看它们解决了什么样的问题。
简化自定义错误
在其他语言中,自定义错误非常简单,以 Go 为例,只需要实现 error 接口即可:
import "fmt"
type NotFoundError struct {
Resource string
}
func (e NotFoundError) Error() string {
return fmt.Sprintf("%s not found", e.Resource)
}
而在 Rust 中,自定义错误就比较麻烦了,需要手动实现 Error、Display 和 Debug 这三个特征。如果需要支持错误自动转换,即适配 ? 操作符,还需要额外实现 From 特征:
use std::error::Error;
use std::{fmt, io, num::ParseIntError};
#[derive(Debug)]
enum MyError {
Io(io::Error),
Parse(ParseIntError),
Custom(String),
}
// 实现 Display 特征
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MyError::Io(e) => write!(f, "IO 错误: {}", e),
MyError::Parse(e) => write!(f, "解析错误: {}", e),
MyError::Custom(s) => write!(f, "自定义错误: {}", s),
}
}
}
// 实现 Error 特征,标记该类型为错误类型
impl Error for MyError {}
// 实现 From 特征,支持错误自动转换
impl From<io::Error> for MyError {
fn from(e: io::Error) -> Self {
MyError::Io(e)
}
}
impl From<ParseIntError> for MyError {
fn from(e: ParseIntError) -> Self {
MyError::Parse(e)
}
}
可以看到,仅是定义一个简单的自定义错误枚举,就需要编写大量的样板代码。而 thiserror 库则通过宏解决了这一问题。它能在编译期自动生成上述所有样板代码,让我们专注于错误本身的定义。
我们用 thiserror 来改写上面的示例,实现如下:
use std::{io, num::ParseIntError};
use thiserror::Error;
#[derive(Error, Debug)]
enum MyError {
// 定义 Display 输出格式,{0} 引用变体第一个字段
#[error("IO 错误: {0}")]
// #[from] 自动实现 From 特征
Io(#[from] io::Error),
#[error("解析错误: {0}")]
Parse(#[from] ParseIntError),
#[error("自定义错误: {0}")]
Custom(String),
}
需要注意的是,thiserror 是基于宏实现,所有代码生成都在编译期完成,运行时零开销,不会对程序性能带来影响。因此,在需要自定义错误类型的场景中,可以大胆的使用 thiserror。
简化通用错误处理
在实际开发中,我们常常会遇到函数返回多种不同错误类型的场景。此时有两种处理思路:一是定义全局自定义错误枚举,也就是上一章节的方案,可以通过 thiserror 简化。二是使用 Box<dyn Error> 作为错误返回类型,无需单独定义错误枚举。
use std::error::Error;
use std::fs;
fn read_and_parse(path: &str) -> Result<i32, Box<dyn Error>> {
let content = fs::read_to_string(path)?;
let num: i32 = content.trim().parse()?;
Ok(num)
}
先简单了解下 Box<dyn Error>,由于所有的错误类型都实现 Error 特征,dyn Error 作为动态派发的特征对象,所以可以兼容所有错误类型。
然而由于 dyn Error 是不定长类型(DST),无法直接存储在栈上,因此需要用 Box<T> 智能指针将其分配到堆上。
最后是标准库中已为所有实现 Error 特征的类型实现了 From 转换,因此可以直接使用 ? 操作符自动转换错误类型。标准库实现大致如下:
impl<E: Error + 'static> From<E> for Box<dyn Error> {
fn from(err: E) -> Self {
Box::new(err)
}
}
搞懂了 Box<dyn Error> 后,现在我们就可以讲 anyhow 了,anyhow 的底层实现是:
Box<dyn Error + Send + Sync + 'static>
不难看出,anyhow 是 Box<dyn Error> 的增强版,具体如下:
简洁的类型别名
anyhow 提供 anyhow::Result<T> 类型别名,简化代码:
use anyhow::Result;
fn main() -> Result<()> {
Ok(())
}
携带业务上下文
anyhow 内置的 context 和 with_context 方法,它们为错误添加上下文:
use anyhow::{Context, Result};
use std::fs;
fn read_and_parse(path: &str) -> Result<i32> {
let content = fs::read_to_string(path)?;
let num: i32 = content.trim().parse()?;
Ok(num)
}
fn main() -> anyhow::Result<()> {
let content = fs::read_to_string("config.json")
.context("读取配置文件失败")?;
println!("{}", content);
Ok(())
}
// Output:
// Error: 读取配置文件失败
// Caused by:
// No such file or directory (os error 2)
支持多线程与异步
Box<dyn Error> 未约束 Send + Sync,所以在多线程或异步场景下时会直接报错。而 anyhow 强制约束了 Send + Sync,开箱即用,完全兼容多线程与异步场景:
use std::thread;
fn do_work() -> anyhow::Result<()> {
Err(anyhow::anyhow!("something wrong"))
}
fn main() {
let handle = thread::spawn(|| do_work());
let result = handle.join().unwrap();
match result {
Ok(_) => println!("success"),
Err(e) => println!("error: {:#}", e),
}
}
// Output:
// error: something wrong
结语
在这篇文章中,我们介绍了 thiserror 和 anyhow 的使用方法,同时讲解了它们的设计初衷,解决 Rust 原生错误处理的哪些痛点。学习技术时,知其然更要知其所以然,理解它们为什么存在,才能在实际开发中灵活运用,选择最适合的方案。
到此这篇关于Rust 错误处理的黄金搭档(thiserror和anyhow)的文章就介绍到这了,更多相关Rust 错误处理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
Rust开发环境搭建到运行第一个程序HelloRust的图文教程
本文主要介绍了Rust开发环境搭建到运行第一个程序HelloRust的图文教程,文中通过图文介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧2023-12-12
前端基于Rust实现的Wasm进行图片压缩的技术文档(实现方案)
在现代Web开发中,利用Rust编写的图片压缩代码可以编译成WebAssembly(Wasm)模块,Rust的内存安全特性和Wasm的跨平台能力,使得这种方案既高效又安全,对Rust Wasm图片压缩实现方案感兴趣的朋友一起看看吧2024-09-09


最新评论