读到一篇非常好的文章baoyachi
大佬的<细说Rust错误处理>从Rust中怎么处理错误, 讲到怎么定义自己的错误类型, 再到如何简化错误处理流程, 再到如何统一错误处理的形式. 但是这些都是基于标准库提供功能实现的, 需要手动写一些模板代码来完成这些简化流程.
但是, 能偷懒的地方当然要偷懒. 如何才能将这些公式化的代码, 用更简便的方式实现呢? thiserror
和anyhow
就是将我们需要手动实现的部分, 使用派生和宏实现了.
这篇文章是想对比手动实现的过程理解thiserror
和anyhow
到底实现了怎样的处理. 希望能做到: 知其然, 并知其所以然.
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
自己的错误.rust中实现自己的错误类型需要三个步骤:
(1). 为自定义错误类型手动实现
impl std::fmt::Display
并实现fn fmt
(2). ~手动实现
impl std::fmt::Debug
, 直接使用#[derive(Debug)]
即可(3). ~手动实现
impl std::error::Error
并根据自身error
级别选择是否覆盖std::error::Error
中的source
方法ChildError
为子类型Error
,不覆盖source()
方法,空实现std::error::Error
CustomError
有子类型ChildError
,覆盖source()
,并返回子类型Option值(就是返回错误值):Some(&self.err)
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), <--这些就相当于ChildError 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), } } } ///实现Error的trait,因为有子Error:ChildError,需要覆盖source()方法,返回Some(err) 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), } } } #[derive(Debug)] <-- 实现 std::fmt::Debug struct ChildError; ///实现Display的trait,并实现fmt方法 impl std::fmt::Display for ChildError { <-- 实现 std::fmt::Display fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "ChildError is here!") } } ///实现Error的trait,因为没有子Error,不需要覆盖source()方法 impl std::error::Error for ChildError {} <--实现std::error::Error
-
自定义error转换
定义了自己的错误类型, 下来就是如何将其他的错误类型都归拢到"我的"错误类型之下,为我所用. 就是使用
From Trait
.std::io::Error
,std::str::Utf8Error
和std::num::ParseIntError
等多种错误类型都统一到自定义错误CustomError
下.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(()) }
-
简化模板代码
在上面的代码中, 没有看到
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>;
总结一下:
实现自定义的错误类型, 需要为错误类型实现Display Debug Error
三个Trait
如果想要将其他的错误类型都收拢到自定的Trait
下, 就为这些类型实现From Trait
如果类型定义很长, 那就可以定义一个newtype
来换成模板的替换
anyhow帮我们实现了什么
-
anyhow
提供了一个Result
和Error
, 直接实现了一个错误类型, 也实现了错误类型的转换使用
anyhow::Result<T>
或者使用Result<T, anyhow::Error>
. 他将作为会出错函数的返回值使用. 这相当于帮我们实现了一个统一的错误类型(你们别自己定义了, 直接用我的就行). 我们只需要管好正常值就行, 错误处理都交给anyhow
.use anyhow::Result fn get_cluster_info() -> Result<ClusterMap> { <-只关注正常返回值的类型即可 let config = std::fs::read_to_string("cluster.json")?; <-其他的错误类型, anyhow都实现了From Trait 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::read_to_string(path).with_context(|| format!("Failed to read file at {}", path)) }
thiserror帮我们实现了什么
-
为自定义错误类型, 实现了
std::fmt::Display
. 通过在类型定义中使用#[error("...")]
, 实现了错误信息的定义.use thiserror::Error; #[derive(Error, Debug)] <-派生thiserror的Error. pub enum DataStoreError { #[error("data store disconnected")] <-定义错误信息 Disconnect(#[from] 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, }
速记语法是:
#[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, }
总结
理清前后的脉络之后, 就可以看到thiserror
和anyhow
的作用就是帮我们简化大量的模板代码. 是对我们手动实现自己的错误的抽象. 这样也能理解crate
中功能的作用了.