前言
错误处理在生产级别的代码中一直都是一个重点。在原型阶段,愉快地使用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这一著名的错误处理库做了初步的介绍,祝大家一起在错误中成长~
评论区
写评论目前用
anyhow
/thiserror
的比较多这个确实挺规范的