std::io::Error, thiserror和anyhow
读到一篇非常好的文章baoyachi大佬的<细说Rust错误处理>从Rust中怎么处理错误, 讲到怎么定义自己的错误类型, 再到如何简化错误处理流程, 再到如何统一错误处理的形式. 但是这些都是基于标准库提供功能实现的, 需要手动写一些模板代码来完成这些简化流程.
但是, 能偷懒的地方当然要偷懒. 如何才能将这些公式化的代码, 用更简便的方式实现呢? thiserror和anyhow就是将我们需要手动实现的部分, 使用派生和宏实现了.
这篇文章是想对比手动实现的过程理解thiserror和anyhow到底实现了怎样的处理. 希望能做到: 知其然, 并知其所以然. 希望您能先读一下baoyachi大佬的文章, 再看这篇文章.
rust错误处理的示例
rust
use std::io::Error;
fn main() {
let path = "/tmp/dat"; //文件路径
match read_file(path) { //判断方法结果
Ok(file) => { println!("{}", file) } //OK 代表读取到文件内容,正确打印文件内容
Err(e) => { println!("{} {}", path, e) } //Err代表结果不存在,打印错误结果
}
}
fn read_file(path: &str) -> Result<String,Error> { //Result作为结果返回值
std::fs::read_to_string(path) //读取文件内容
}
rust中通常是使用Result作为返回值, 通过Result来判断执行的结果. 并使用match匹配的方式来获取Result的内容,是正常或错误[引用自第4.2节].
Rust中的错误处理步骤
-
自定义error
在自己的
bin crate或者lib crate当中, 如果是为了完成一个项目, 通常会实现自己的错误类型. 一是方便统一处理标准库或者第三方库中抛出的错误. 二是可以在最上层方便处理当前crate自己的错误.手动实现
impl std::fmt::Display并实现fn fmt手动实现
impl直接使用#[derive(Debug)]即可手动实现
impl std::error::Error并根据自身error级别是否覆盖std::error::Error中的source方法ChildError为子类型Error,没有覆盖source()方法,空实现了std::error::ErrorCustomError有子类型ChildError,覆盖 了source(),并返回了子类型Option值:Some(&self.err)
-
自定义error转换
大佬的文章中举例了, 如何将
std::io::Error,std::str::Utf8Error和std::num::ParseIntError统一到自定义错误CustomError下.rustuse std::error::Error; use std::io::Error as IoError; use std::str::Utf8Error; use std::num::ParseIntError; use std::fmt::{Display, Formatter}; #[derive(Debug)] <-- 实现 std::fmt::Debug enum CustomError { ParseIntError(std::num::ParseIntError), <--重新包装一下 Utf8Error(std::str::Utf8Error), IoError(std::io::Error), } impl Display for CustomError { <-- 实现 std::fmt::Display fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match &self { CustomError::IoError(ref e) => e.fmt(f), CustomError::Utf8Error(ref e) => e.fmt(f), CustomError::ParseIntError(ref e) => e.fmt(f), } } } impl std::error::Error for CustomError { <--实现std::error::Error fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { <--实现source方法 match &self { CustomError::IoError(ref e) => Some(e), CustomError::Utf8Error(ref e) => Some(e), CustomError::ParseIntError(ref e) => Some(e), } } } impl From<ParseIntError> for CustomError { <--from trait实现转换 fn from(s: std::num::ParseIntError) -> Self { CustomError::ParseIntError(s) } } impl From<IoError> for CustomError { <--from trait实现转换 fn from(s: std::io::Error) -> Self { CustomError::IoError(s) } } impl From<Utf8Error> for CustomError { <--from trait实现转换 fn from(s: std::str::Utf8Error) -> Self { CustomError::Utf8Error(s) } }经过改造错误处理会变成
rustfn main() -> std::result::Result<(),CustomError> { let path = "./dat"; let v = read_file(path)?; <--直接使用?操作符, 当报错时会将错误转换成CustomError let x = to_utf8(v.as_bytes())?; <--都转换成一个错误类型, 方便统一的处理. 这里使用? 省去了使用match那样的层级结构. let u = to_u32(x)?; println!("num:{:?}",u); Ok(()) }同样这种方式可以使用在单元测试上.
rust#[cfg(test)] mod tests { use super::*; #[test] fn test_get_num() -> std::result::Result<(), CustomError> { let path = "./dat"; let v = read_file(path)?; let x = to_utf8(v.as_bytes())?; let u = to_u32(x)?; assert_eq!(u, 8); Ok(()) } } -
简化模板代码
在上面的代码中, 没有看到
read_file,to_utf8,to_u32的实现, 下面看看这些函数的签名rustfn read_file(path: &str) -> std::result::Result<String, CustomError> {} fn to_utf8(v: &[u8]) -> std::result::Result<&str, CustomError> {} fn to_u32(v: &str) -> std::result::Result<u32, CustomError> {}其中对
Result的声明很繁琐, 能不能简化? 可以重新定义一个类型来简化输入rustpub type IResult<I> = std::result::Result<I, CustomError>;这样可以简化成
rustfn read_file(path: &str) -> IResult<String> {} fn to_utf8(v: &[u8]) -> IResult<&str> {} fn to_u32(v: &str) -> IResult<u32> {}多参数的类型为
rustpub type IResult<I, O> = std::result::Result<(I, O), CustomError>;
thiserror帮我们实现了什么
-
实现了
std::fmt::Displaythiserror通过#[error("...")]实现了Display trait. 速记语法是:#[error("{var}")]-⟶write!("{}", self.var)#[error("{0}")]-⟶write!("{}", self.0)#[error("{var:?}")]-⟶write!("{:?}", self.var)#[error("{0:?}")]-⟶write!("{:?}", self.0)
-
实现了
From trait通过
#[from]为错误类型实现From trait. 这个变体不能含有除source error以及可能的backtrace之外的字段. 如果存在backtrace字段, 则将从From impl中捕获backtrace.rust#[derive(Error, Debug)] pub enum MyError { Io { #[from] source: io::Error, backtrace: Backtrace, } } -
实现对
source()的覆盖可以使用
#[source]属性,或者将字段命名为source,来为自定义错误实现source方法,返回底层的错误类型.rust#[derive(Error, Debug)] pub struct MyError { msg: String, #[source] // 如果字段的名称是source, 这个标签可以不写 source: anyhow::Error, } #[derive(Error, Debug)] pub struct MyError { msg: String, #[source] // 或者标记名称非source的字段 err: anyhow::Error, }
anyhow帮我们实现了什么
-
anyhow提供了一个Result和Error, 直接实现了一个错误类型, 也实现了错误类型的转换anyhow::Result<T>或者使用Result<T, anyhow::Error>. 他将作为会出错函数的返回值使用. 这相当于帮我们实现了一个统一的错误类型(你们别自己定义了, 直接用我的就行).rustuse anyhow::Result fn get_cluster_info() -> Result<ClusterMap> { let config = std::fs::read_to_string("cluster.json")?; let map: ClusterMap = serde_json::from_str(&config)?; Ok(map) } -
简单的创建新的错误
使用
anyhow!宏直接创建一个错误信息rustuse anyhow::anyhow; return Err(anyhow!("Missing attribute: {}", missing));bail!宏是上面形式的更简化表示rustbail("Missing attribute: {}", mission);
anyhow也实现了其他的一些功能
使用context, with_context为已有的错误添加更多的说明信息:
rust
use anyhow::{Context, Result};
fn read_file(path: &str) -> Result<String> {
std::fs::tead_to_string(path).with_context(|| format!("Failed to read file at {}", path))
}
总结
理清前后的脉络之后, 就可以看到thiserror和anyhow的作用就是帮我们简化大量的模板代码. 是对我们手动实现自己的错误的抽象. 这样也能理解crate中功能的作用了.