2 个版本

0.0.2 2021年2月28日
0.0.1 2020年11月15日

#1379 in 开发工具

MIT/Apache

18KB
129

概述 & 动机

在 Rust 中,处理错误的标准方式是使用像 Anyhow 这样的 crate 将任何错误简单地向上传递给调用者(并且希望最终传递给能够处理它的代码),通过 ? 操作符。或者为每个模块或 crate 创建一个复杂的自定义错误类型。这种错误类型通常采用枚举的形式,每个变体对应于从该 crate 或模块可能发生的每种错误。

这些方法的问题在于,你的函数的使用者可能不知道每个函数可能发生哪些错误。在 Anyhow 的情况下,这个问题更加严重,因为返回 Anyhow 错误类型的函数可以返回 真正意义上的任何错误。在某些情况下这可能可以接受,但这显然不是理想的。当你创建自定义错误类型时,有时这不是一件简单的事情,这可能是大多数 crate 没有为每个函数提供错误类型的原因。将这种错误处理方式与 Java 伪代码中的方式进行比较

public Object readAndParseFile(Path file) throws IOException, ParseError{
    String contents = Files.read(file); //throws IOException
    Object parsedContents = parseString(contents); //throws ParseError
    return parsedContents;
}

在这个例子中,可能出现的失败模式在函数的签名中得到了明确的文档记录。此外,Javadoc 注释通常描述在哪些场景下会抛出每个异常。你可以在这里了解更多关于我对 Rust 错误处理系统与 Java 相比的看法 here

Polyerror通过使其变得如此简单(实际上是一个单行宏调用)来定义一个舒适(?工作)且正确的错误类型,以至于为每个函数有一个单独的错误类型是实用的。这样,对于最终用户来说,总是很明显函数可能会以哪种方式出错。请参阅示例部分以获取更多详细信息并了解如何使用此crate。此crate的另一个优点是它非常简单易用——一旦您阅读了本文档,您就了解了有关crate的所有信息,与Rust的其他错误库相比。仅导出了一个宏,它提供了您在Rust中进行健壮、易于理解(对您和您的用户而言)且正确的错误处理所需的一切。

示例

宏会展开成什么样子?

考虑以下Rust代码(基于此crate的基本_use测试)

use std::str::ParseBoolError;
use std::num::ParseIntError;

create_error!(pub ParseThenCombineError: ParseBoolError, ParseIntError);
pub fn parse_then_combine(a: &str, b: &str) -> Result<String, ParseThenCombineError> {
    let parsed_bool: bool = a.parse()?;
    let parsed_int: i32 = b.parse()?;
    Ok(format!("{} {}", parsed_bool, parsed_int))
}

在这个玩具函数中,有两个可能的错误:ParseBoolError和ParseIntError(都来自标准库)。在使用传统模式的情况下,您可能需要使用类似Anyhow的东西,这样就会从您的crate的用户那里隐藏有用的(如果不是至关重要的)信息,迫使他们深入研究您的代码或希望它得到了适当的文档(这让我想起了C和C++中如何指定生命周期),或者简单地将这些两个错误类型添加到一个全局错误枚举中。在这里,我们将仅使用ParseThenCombineError(如果您的风格偏好,您可以自由选择更简洁的名称)来处理parse_then_combine函数。由于它是在函数之前单行定义的,因此这并不会带来任何显著的繁琐或生产力损失。

为了准确了解正在发生的事情,上面的create_error!调用会展开成以下源代码

#[derive(Debug)]
pub enum ParseThenCombineError {
    ParseBoolError(ParseBoolError),
    ParseIntError(ParseIntError),
}
impl ::std::fmt::Display for ParseThenCombineError {
    fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
        write!(f, "{:?}", self)
    }
}
impl ::std::error::Error for ParseThenCombineError {}

impl ::std::convert::From<ParseBoolError> for ParseThenCombineError {
    fn from(error: ParseBoolError) -> Self {
        Self::ParseBoolError(error)
    }
}

impl ::std::convert::From<ParseIntError> for ParseThenCombineError {
    fn from(error: ParseIntError) -> Self {
        Self::ParseIntError(error)
    }
}

注意

  • create_error!生成的Rust展开代码将精确地引用您指定的各种错误类型。例如,如果提供了完整的路径(例如std::num::ParseIntError),那么产生的Rust代码也会使用该完整路径。此外,变体的名称将是StdNumParseIntError。此外,如果名称中有下划线,例如actix_web::Error,那么这些下划线将被适当地转换成枚举变体名称:ActixWebError
  • 您可以使用任何有效的访问修饰符,包括无(通常用于私有继承),pub(crate),等等。
  • 创建的错误类型在docs.rs中进行了文档说明,就像任何手动创建的错误类型一样,使其使用变得简单。
  • 请注意,parse_then_combine的返回类型不是一个类型别名。当使用此crate时,这更可取,因为每个错误类型只使用一次。

待办事项

  • 基本功能
  • 允许从函数返回错误,这些错误不是简单上游错误的包装(欢迎PR,但请先提出一个问题来讨论语法)。在此期间,您可以创建一个更传统的错误类型,用于那些不是简单包装上游错误的错误。
  • 在构建时包含回溯(最好等到Rust标准库中的回溯稳定,尽管有一个crate在理论上可以在此期间处理它)

依赖项

~1.5MB
~37K SLoC