std::io::Error, thiserror和anyhow
读到一篇非常好的文章baoyachi
大佬的<细说Rust错误处理>从Rust中怎么处理错误, 讲到怎么定义自己的错误类型, 再到如何简化错误处理流程, 再到如何统一错误处理的形式. 但是这些都是基于标准库提供功能实现的, 需要手动写一些模板代码来完成这些简化流程.
但是, 能偷懒的地方当然要偷懒. 如何才能将这些公式化的代码, 用更简便的方式实现呢? thiserror
和anyhow
就是将我们需要手动实现的部分, 使用派生和宏实现了.
这篇文章是想对比手动实现的过程理解thiserror
和anyhow
到底实现了怎样的处理. 希望能做到: 知其然, 并知其所以然. 希望您能先读一下baoyachi
大佬的文章, 再看这篇文章.
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::Error
CustomError
有子类型ChildError
,覆盖了source()
,并返回了子类型Option值:Some(&self.err)
-
自定义error转换
大佬的文章中举例了, 如何将
std::io::Error
,std::str::Utf8Error
和std::num::ParseIntError
统一到自定义错误CustomError
下.use 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) } }
经过改造错误处理会变成
fn 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(()) }
同样这种方式可以使用在单元测试上.
#[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
的实现, 下面看看这些函数的签名fn 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
的声明很繁琐, 能不能简化? 可以重新定义一个类型来简化输入pub type IResult<I> = std::result::Result<I, CustomError>;
这样可以简化成
fn read_file(path: &str) -> IResult<String> {} fn to_utf8(v: &[u8]) -> IResult<&str> {} fn to_u32(v: &str) -> IResult<u32> {}
多参数的类型为
pub type IResult<I, O> = std::result::Result<(I, O), CustomError>;
thiserror帮我们实现了什么
-
实现了
std::fmt::Display
thiserror
通过#[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
.#[derive(Error, Debug)] pub enum MyError { Io { #[from] source: io::Error, backtrace: Backtrace, } }
-
实现对
source()
的覆盖可以使用
#[source]
属性,或者将字段命名为source
,来为自定义错误实现source
方法,返回底层的错误类型.#[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>
. 他将作为会出错函数的返回值使用. 这相当于帮我们实现了一个统一的错误类型(你们别自己定义了, 直接用我的就行).use 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!
宏直接创建一个错误信息use anyhow::anyhow; return Err(anyhow!("Missing attribute: {}", missing));
bail!
宏是上面形式的更简化表示bail("Missing attribute: {}", mission);
anyhow也实现了其他的一些功能
使用context
, with_context
为已有的错误添加更多的说明信息:
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
中功能的作用了.