< 返回版块

黑豆腐 发表于 2020-03-27 10:12

Tags:rust

前言

错误处理在生产级别的代码中一直都是一个重点。在原型阶段,愉快地使用unwrap可以确保思路和精力被集中用在业务逻辑开发上。不过对于最终要上线的代码,优雅的处理错误却是至关重要的。原生Rust错误处理的工具有std::error::Error(一般我们会看到Box<dyn Error>的形式),?操作符以及enum供我们自定义错误类型。这本身就可以作为一个专题来讨论。而今天我们就来简单介绍一下failure库以及其背后的错误处理哲学。

简介

failure是rust-lang-nursery下的一个库,可以说是根正苗红的rust库了。其目标是取代基于std::eror::Error的错误处理。

failure有两个核心组件

  • Fail: 定制错误类型用的trait
  • Error: 只要实现了Fail,就能转化为该结构体

Fail trait

Fail trait被用来取代std::error::Error。它提供了backtrace和cause方法去获取错误的信息。也支持将错误包装在上下文(context)中。所有新的错误类型都应该实现该trait。 对于一个实现了Fail的fail,我们可以逐层打印出错误链条

let mut fail: &Fail = err;

while let Some(cause) = fail.cause() {
    println!("{}", cause);

    fail = cause;
}

也可以打印出backtrace错误回溯

if let Some(bt) = err.cause().and_then(|cause| cause.backtrace()) {
    println!("{}", bt)
}

也可以引入ResultExt从而给错误加入上下文(context)以分辨想通的错误。比如为了区分io错误,我们可以:

use failure::ResultExt;

let mut file = File::open(cargo_toml_path).context("Missing Cargo.toml")?;
file.read_to_end(&buffer).context("Could not read Cargo.toml")?;

自动推导Fail

一般来说Fail可以方便的被derive出来,只需要自己去实现Display即可

extern crate failure;
#[macro_use] extern crate failure_derive;

use std::fmt;

#[derive(Fail, Debug)]
struct MyError;

impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "An error occurred.")
    }
}

Error

而只要实现了Fail trait的类型,都可以通过?被转化成Error。比如下边的例子中,IO错误和JSON错误都被转化成了Result中的Error

#[derive(Deserialize)]
struct Object {
    ...
}

impl Object {

    fn from_file(path: &Path) -> Result<Object, Error> {
        let mut string = String::new();
        File::open(path)?.read_to_string(&mut string)?;
        let object = json::from_str(&string)?;
        Ok(object)
    }
}

几个模式

简单说来,有如下几个模式可供选择:

  • 使用字符串:比较适合原型计算。
  • 定义自己的Fail实现:定义一个自己的错误类型,比较适合需要对错误有较多控制的库。
  • 使用Error:使用Error统一处理多处不同的返回错误。比较适合不太需要详细检查error内容的应用或库。
  • 使用Error和ErrorKind对:使用Error类型和ErrorKind枚举创建一个健壮的错误类型。比较适合作为大型库的公共API。

使用字符串

这是一个比较简便的方法,推荐在原型阶段使用。format_err宏可以很容易的生成一个Error。

fn check_range(x: usize, range: Range<usize>) -> Result<usize, Error> {
    if x < range.start {
        return Err(format_err!("{} is below {}", x, range.start));
    }
    if x >= range.end {
        return Err(format_err!("{} is above {}", x, range.end));
    }
    Ok(x)
}

除了原型阶段之外,当错误非常罕见以及对错误的处理只能局限于打印的情况下,可以使用这个模式。 很明显,在这个模式下能对返回的错误做出的操作非常有限。如果需要知道错误的具体类型/内容,还需要做字符串匹配。

定义自己的Fail实现

接下来就是自定义实现了Fail trait的错误,这可以通过derive宏很容易的实现。这样做有三大好处:

  • 可以遍历所有的错误
  • 可以完全控制错误的表达
  • 调用者可以直接析构出错误 可以看到在例子中,我们可以给错误增加想要的信息。
#[derive(Fail, Debug)]
#[fail(display = "Input was invalid UTF-8 at index {}", index)]
pub struct Utf8Error {
    index: usize,
}

在对待从依赖以外的部分返回的错误是,可以使用这一策略。

使用Error

当一个函数中会返回多种错误时可以使用这一模式,其具有以下特点:

  • 开始时不需要自定义类型
  • 实现了Fail trait的类型只要使用?操作符就可以变为Error类型
  • 当你引入新的依赖和新的错误类型时,你可以直接抛出它们 要使用该模式,只要把返回值设定为Result<_, Error>
use std::io;
use std::io::BufRead;

use failure::Error;
use failure::err_msg;

fn my_function() -> Result<(), Error> {
    let stdin = io::stdin();

    for line in stdin.lock().lines() {
        let line = line?;

        if line.chars().all(|c| c.is_whitespace()) {
            break
        }

        if !line.starts_with("$") {
            return Err(format_err!("Input did not begin with `$`"));
        }

        println!("{}", &line[1..]);
    }

    Ok(())
}

一般来说当你不需要析构返回的错误时可以采用这一模式。包括:

  • 原型设计时
  • 绝大多数时间你只会打印错误的时候
  • 不需要得到关于错误的更多信息的时候

使用Error和ErrorKind对

这是最健壮的处理错误的方式,当然也需要更多的维护成本。它相当于结合了定义自己的Fail实现和使用Error的优点:

  • 和Error一样向前兼容新的错误类型,而依赖中的错误已经可以被转化成该错误类型
  • 像自定义Fail一样可以给错误提供额外的信息,而用户很容易得到这些信息 你需要创建Error类型以及ErrorKind枚举:
#[derive(Debug)]
struct MyError {
    inner: Context<MyErrorKind>,
}

#[derive(Copy, Clone, Eq, PartialEq, Debug, Fail)]
enum MyErrorKind {

    #[fail(display = "A contextual error message.")]
    OneVariant,
    // ...
}

但是你必须自己实现Fail,以及提供一些转换函数

impl Fail for MyError {
    fn cause(&self) -> Option<&Fail> {
        self.inner.cause()
    }

    fn backtrace(&self) -> Option<&Backtrace> {
        self.inner.backtrace()
    }
}

impl Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        Display::fmt(&self.inner, f)
    }
}

impl MyError {
    pub fn kind(&self) -> MyErrorKind {
        *self.inner.get_context()
    }
}

impl From<MyErrorKind> for MyError {
    fn from(kind: MyErrorKind) -> MyError {
        MyError { inner: Context::new(kind) }
    }
}

impl From<Context<MyErrorKind>> for MyError {
    fn from(inner: Context<MyErrorKind>) -> MyError {
        MyError { inner: inner }
    }
}

这样你就可以使用把ErrorKind注入到使用的函数中

perform_some_io().context(ErrorKind::NetworkFailure)?;

当然你也可以直接抛出错误

Err(ErrorKind::DomainSpecificError)?

该模式最适合处于中间层并且有许多依赖的生产环境代码。

小结

错误的坑真是深似水,这里也只是对failure这一著名的错误处理库做了初步的介绍,祝大家一起在错误中成长~

评论区

写评论
jetli 2020-03-27 13:43

目前用anyhow/thiserror的比较多

phper-chen 2020-03-27 12:29

这个确实挺规范的

1 共 2 条评论, 1 页