std::io::Error, thiserror和anyhow

std::io::Error, thiserror和anyhow

读到一篇非常好的文章baoyachi大佬的<细说Rust错误处理>从Rust中怎么处理错误, 讲到怎么定义自己的错误类型, 再到如何简化错误处理流程, 再到如何统一错误处理的形式. 但是这些都是基于标准库提供功能实现的, 需要手动写一些模板代码来完成这些简化流程.

但是, 能偷懒的地方当然要偷懒. 如何才能将这些公式化的代码, 用更简便的方式实现呢? thiserroranyhow就是将我们需要手动实现的部分, 使用派生和宏实现了.

这篇文章是想对比手动实现的过程理解thiserroranyhow到底实现了怎样的处理. 希望能做到: 知其然, 并知其所以然. 希望您能先读一下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中的错误处理步骤

  1. 自定义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)
  2. 自定义error转换

    大佬的文章中举例了, 如何将std::io::Error, std::str::Utf8Errorstd::num::ParseIntError统一到自定义错误CustomError 下.

    rust 复制代码
    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)
        }
    }

    经过改造错误处理会变成

    rust 复制代码
    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(())
    }

    同样这种方式可以使用在单元测试上.

    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(())
        }
    }
  3. 简化模板代码

    在上面的代码中, 没有看到read_file, to_utf8, to_u32的实现, 下面看看这些函数的签名

    rust 复制代码
    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的声明很繁琐, 能不能简化? 可以重新定义一个类型来简化输入

    rust 复制代码
    pub type IResult<I> = std::result::Result<I, CustomError>;

    这样可以简化成

    rust 复制代码
    fn read_file(path: &str) -> IResult<String> {}
    fn to_utf8(v: &[u8]) -> IResult<&str> {}
    fn to_u32(v: &str) -> IResult<u32> {}

    多参数的类型为

    rust 复制代码
    pub type IResult<I, O> = std::result::Result<(I, O), CustomError>;

thiserror帮我们实现了什么

  1. 实现了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)
  2. 实现了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,
        }
    }
  3. 实现对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帮我们实现了什么

  1. anyhow提供了一个ResultError, 直接实现了一个错误类型, 也实现了错误类型的转换

    anyhow::Result<T> 或者使用 Result<T, anyhow::Error>. 他将作为会出错函数的返回值使用. 这相当于帮我们实现了一个统一的错误类型(你们别自己定义了, 直接用我的就行).

    rust 复制代码
    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)
    }
  2. 简单的创建新的错误

    使用anyhow!宏直接创建一个错误信息

    rust 复制代码
    use anyhow::anyhow;
    
    return Err(anyhow!("Missing attribute: {}", missing));

    bail!宏是上面形式的更简化表示

    rust 复制代码
    bail("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))
}

总结

理清前后的脉络之后, 就可以看到thiserroranyhow的作用就是帮我们简化大量的模板代码. 是对我们手动实现自己的错误的抽象. 这样也能理解crate中功能的作用了.

相关推荐
shimly1234566 小时前
(done) 速通 rustlings(9) 分支跳转
rust
shimly12345612 小时前
(done) 速通 rustlings(4) 变量声明
rust
shimly12345613 小时前
(done) 速通 rustlings(11) 向量vector及其操作
rust
shimly12345613 小时前
(done) 速通 rustlings(3) intro1 println!()
rust
shimly12345613 小时前
(done) 速通 rustlings(12) 所有权
rust
shimly12345615 小时前
(done) 速通 rustlings(7) 全局变量/常量
rust
敲敲了个代码15 小时前
构建工具的第三次革命:从 Rollup 到 Rust Bundler,我是如何设计 robuild 的
开发语言·前端·javascript·后端·rust
lpfasd12315 小时前
Tauri 中实现自更新(Auto Update)
rust·tauri·update
shimly12345615 小时前
(done) 速通 rustlings(10) 基本数据类型
rust
shimly12345615 小时前
(done) 速通 rustlings(8) 函数
rust