跳转至

错误处理

参考Rust Book,错误处理可以分成两种,可恢复的错误和不可恢复的错误。可恢复的错误对应Result,不可恢复的错误对应 panic

Result

Result 是可恢复错误处理的基础,常见的对 Result 的简单处理有以下方法:

result.unwrap();        // 适合相信一定不会失败,或者写demo
result.expect("xxx");   // 适合失败了也没有办法处理,报个错panic
// 适合失败了也可以处理
if let Ok(x) = result {
    todo!();
} else {
    todo!();
}
result.unwrap_or(); // 失败了就给个默认值

根据编码规范的建议,应该在会出现错误的函数,以及会 Panic 的函数添加什么时候会出现错误的说明。

#![warn(clippy::missing_errors_doc)]
#![warn(clippy::missing_panics_doc)]

/// # Errors
///
/// Will return `Err` if ...
/// 
/// # Panics
///
/// Will panic if ...
pub fn read(filename: String) -> io::Result<String> {
    unimplemented!();
}

? 和 Try

?是Rust提供的语法糖,让我们免于编写错误处理函数,而是直接将错误返回,让调用者负责处理错误。

需要注意的是,? 表达式返回的错误类型需要和函数参数匹配,不是所有的返回 Result 的函数都能用 ? 一把梭。虽然你可以用 Result<T,Box<dyn Error>>,但这样并不好。可以用下面介绍的函数式做一些处理。

如果为了省事,可以用 Anyhow 这个库。你就可以随便用 ? 了。更科学的做法是为自己程序可能发生的错误定义好错误类型,thiserror 库可以帮忙。

下面探究一下 ? 的内部实现,首先需要了解 trait Try

pub trait Try: FromResidual<Self::Residual> {
    type Output;
    type Residual;
    fn from_output(output: Self::Output) -> Self;
    fn branch(self) -> ControlFlow<Self::Residual, Self::Output>;
}
pub enum ControlFlow<B, C = ()> {
    Continue(C),
    Break(B),
}

当我们在写 x? 的时候,其实我们的意思是。

// x: X
match x.branch() {
    ControlFlow::Continue(a) => a,
    ControlFlow::Break(a) => return X::from_output(a)
}

不难知道 Result 应该实现了 Try Trait. 下面是实现代码。

// 和标准库有出入https://doc.rust-lang.org/src/core/result.rs.html#1893-1900
impl <T,E> ops::Try for Result<T,E> {
    type Output = T;
    type Residual = Result<convert::Infallible, E>;

    fn from_output(output: Self::Output) -> Self {
        Ok(output)
    }

    fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {
        match self {
            Ok(v) => ControlFlow::Continue(v),
            Err(e) => ControlFlow::Break(Err(e))
        }
    }
}

函数式处理

下面是一系列“语法糖”,其实都可以用 match 做进一步的处理。比如有下面四个

方法 原型 说明
and pub fn and<U>(self, res: Result<U, E>) -> Result<U, E> Returns res if the result is Ok, otherwise returns the Err value of self.
and_then pub fn and_then<U, F>(self, op: F) -> Result<U, E> where F: FnOnce(T) -> Result<U, E>, Calls op if the result is Ok, otherwise returns the Err value of self.
or pub fn or<F>(self, res: Result<T, F>) -> Result<T, F> Returns res if the result is Err, otherwise returns the Ok value of self.
or_else pub fn or_else<F, O>(self, op: O) -> Result<T, F> where O: FnOnce(E) -> Result<T, F>, Calls op if the result is Err, otherwise returns the Ok value of self.

andand_then 在语义上其实非常类似,如果是 Ok 的话再做进一步的处理。不过 and 的参数是进一步处理的 结果,而 and_then 的参数是进一步处理的 函数,也就是说 and_then 可以起到 延迟计算 的效果。如果 and 的进一步处理有一定的代价的时候,可以考虑选择 and_then。oror_else 的关系也是完全类似的。

除此外还有 map, map_or, map_err, mar_or_else, unwrap, unwrap_or, unwrap_or_else,可以帮你把错误处理处理出花儿来。

Panic

默认情况(std)下,panic将打印失败消息、展开、清理堆栈并退出。通过环境变量,您还可以让 Rust 在发生 panic 时显示调用堆栈,以便更轻松地追踪恐慌的来源。

不过程序是允许我们自定义 panic handler 的,虽然一般情况不会用到这个特性,

在no_std环境下我们必须自定义 Panic handler,方法也很简单,参考https://doc.rust-lang.org/nomicon/panic-handler.html

#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
    let mut host_stderr = HStderr::new();

    // logs "panicked at '$reason', src/main.rs:27:4" to the host stderr
    writeln!(host_stderr, "{}", info).ok();

    loop {}
}

std 环境中想要自定义panic handler,可以使用std::panic::set_hook方法。

fn main() {
    std::panic::set_hook(Box::new(move |_panic_info| {
        eprintln!("{}", panic_info);
    }));
}

可以借助 color-eyre 的帮忙。

Crates

anyhow

anyhow 是 Rust 语言中一个非常受欢迎的错误处理库,它提供了一种方便的方式来处理错误,特别是在那些不需要特别复杂错误处理的场景中(例如顶层的简单应用)。

它提供了anyhow::Error类型。使用上类似Box<dyn std::error::Error>,任何实现了std::error::Error的类型都可以用它代替(因为实现了AsRef?)。anyhow::Result<T> = Result<T, anyhow::Error>。因此,最简单的用法就是将返回值声明为anyhow::Result,然后碰到Result?

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种进阶的用法,一个是在原先的Error上添加信息供使用者调试,一种是简单的创建一个新的Error。

  • anyhow::Context 提供了两个方法(lazy or not)来给Result添加额外信息。
  • anyhow!("XXX error") 可以用来简单的创建一个Error。

thiserror

thiserror 更适合用于库的开发,其中错误类型通常需要更明确和结构化。thiserror 提供了一个,帮助定义和实现与错误相关的 trait,使得错误处理更加规范和易于维护。

  1. 自定义错误类型: thiserror 允许开发者定义自己的错误类型,每种类型可以关联特定的数据和上下文,这有助于更精确地控制错误处理和传播。
  2. 自动实现 std::error::Error: 使用 thiserror#[derive(Error)] 宏,可以自动为自定义错误类型实现 std::error::Error trait,简化了错误实现的复杂性。
  3. 错误消息模板: thiserror 支持在错误定义中直接使用格式化字符串作为错误消息,这些消息可以包含关联数据的字段,从而提供丰富的错误信息。
  4. 透明的错误转发: thiserror 提供了 #[error(transparent)] 属性,允许错误类型透明地包装其它错误类型,使得原始错误类型和消息可以直接传递出去。

### color eyre

color-eyre 是一个 Rust 库,用于改进错误报告的体验,非常适合用于需要高质量错误报告的应用程序。color-eyre 提供了以下几个关键特性,使得它在错误报告方面表现出色:

  1. 彩色和格式化的错误输出color-eyre 使用彩色和样式化的文本来增强错误消息的可读性。这包括为不同的错误信息部分使用不同的颜色和格式,如错误的类型、源文件、行号等。
  2. 详细的错误链输出:与 anyhoweyre 类似,color-eyre 支持错误链,允许开发者查看导致当前错误的原始错误。color-eyre 以一种易于追踪的方式格式化这些错误链。
  3. 可配置性color-eyre 提供多种配置选项,使开发者可以调整错误报告的样式和内容,如启用或禁用彩色输出,或者更改报告的各个部分的样式。
  4. 回溯支持color-eyre 可以集成 Rust 的回溯信息,即在发生错误时的函数调用堆栈。这对于调试和确定错误发生原因至关重要。