6 个版本
0.1.5 | 2023 年 1 月 11 日 |
---|---|
0.1.4 | 2021 年 10 月 21 日 |
0.1.3 | 2021 年 8 月 31 日 |
0.1.2 | 2020 年 12 月 27 日 |
在 #error-type 中排名 5
每月下载 31 次
25KB
499 行
复合错误
这个 crate 允许你定义方便编写、方便使用、灵活且可扩展的错误。为此,采用组合方法,以便能够在原始错误的基础上定义复杂的错误。
动机
让我们看看在 std::io
中实现的错误处理方法。整个模块只有一个错误类型:std::io::Error
,它有一个 kind()
方法,该方法返回一个 std::io::ErrorKind
。在 std::io
中可能出现错误的所有地方,都会返回一个 Result<T, std::io::Error>
。这使得很容易判断是否发生了错误。然后可以使用 kind()
来确定是哪种错误。然而,std::io::ErrorKind
定义了所有可能发生的错误,即使是那些在特定情况下可能不会发生的错误。例如,它定义了 UnexpectedEof
变体,这可能在 std::fs::File::read()
时发生,但绝对不可能在 std::fs::File::create()
时发生,即使错误类型(std::io::ErrorKind
)在技术上可以是 ErrorKind::UnexpectedEof
。因此,必须在文档注释中说明在特定情况下 std::io::ErrorKind
中的哪些错误变体实际上可能发生,并且因为 Rust 中必须完全匹配,所以即使是不可能发生的错误类型,在错误解决过程中也必须考虑。虽然这是一个错误处理的解决方案,但绝对不是最好的。
另一个选择是为每个函数创建一个错误类型,这些类型可以是只定义实际可能发生的错误变体的枚举。然而,这会导致间接错误的问题。例如,可以想到以下场景:
struct PermissionDenied; // permission denied error
struct FileNotFound; // file not found error
enum OpenError {
FileNotFound(FileNotFound),
PermissionDenied(PermissionDenied)
}
enum CreateError {
PermissionDenied(PermissionDenied)
}
enum OpenOrCreateError { ? }
fn open(...) -> Result<..., OpenError> { ... }
fn create(...) -> Result<..., CreateError> { ... }
fn read_or_create(...) -> Result<..., OpenOrCreateError> { ... }
假设 read_or_create(...)
方法在文件不存在时调用 create()
方法来创建文件,并调用 open()
方法来打开文件。然后它从指定的文件中读取内容,如果没有在打开、创建或读取过程中发生错误,则返回它们。应该如何定义 OpenOrCreateError
?可以为 OpenError
定义一个变体,为 CreateError
定义一个变体,并在进一步编码读取过程中发生的错误时再定义一个,如下所示
struct Read; // error during reading
enum OpenOrCreateError {
OpenError(OpenError),
CreateError(CreateError),
Read(Read)
}
impl From<OpenError> for OpenOrCreateError {
fn from(err: OpenError) -> Self {
Self::OpenError(err)
}
}
impl From<CreateError> for OpenOrCreateError {
fn from(err: CreateError) -> Self {
Self::CreateError(err)
}
}
impl From<ReadError> for OpenOrCreateError {
fn from(err: Read) -> Self {
Self::Read(err)
}
}
这种方法的缺点在于,尽管这是一个完全相同的问题,现在以两种不同的方式进行编码:它可以表示为 OpenOrCreateError::OpenError::PermissionDenied(...)
或 OpenOrCreateError::CreateError::PermissionDenied(...)
。现在调用 read_or_create()
的代码现在必须考虑两种可能发生错误的情况。另一种方法是简化错误层次结构,如下所示
struct Read; // error during reading
enum OpenOrCreateError {
FileNotFound(FileNotFound),
PermissionDenied(PermissionDenied),
Read(Read)
}
impl From<OpenError> for OpenOrCreateError {
fn from(err: OpenError) -> Self {
match err {
OpenError::FileNotFound(fnf) => Self::FileNotFound(fnf),
OpenError::PermissionDenied(pd) => Self::PermissionDenied(pd)
}
}
}
impl From<CreateError> for OpenOrCreateError {
fn from(err: CreateError) -> Self {
match err {
CreateError::PermissionDenied(pd) => Self::PermissionDenied(pd)
}
}
}
impl From<ReadError> for OpenOrCreateError {
fn from(err: Read) -> Self {
Self::Read(err)
}
}
这是一个更好的方法,因为 PermissionDenied
错误现在只以一种方式进行编码。然而,当错误变得更加复杂时,编写起来就变得繁琐,因为所有来自实现的匹配现在都包含所有变体。这正是这个包的作用所在。使用 compound-error
,上述错误可以指定如下
use compound_error::CompoundError;
struct PermissionDenied; // permission denied error
struct FileNotFound; // file not found error
struct Read; // error during reading
#[derive(CompoundError)]
enum OpenError {
FileNotFound(FileNotFound),
PermissionDenied(PermissionDenied)
}
#[derive(CompoundError)]
enum CreateError {
PermissionDenied(PermissionDenied)
}
#[derive(CompoundError)]
enum OpenOrCreateError {
#[compound_error( inline_from(OpenError) )]
FileNotFound(FileNotFound),
#[compound_error( inline_from(OpenError, CreateError) )]
PermissionDenied(PermissionDenied),
Read(Read)
}
为什么还要为 Rust 开发另一个错误实用工具呢?
这是有原因的!大多数其他错误处理包在构建错误层次结构时,而 compound-error
尝试保持错误层次结构扁平,这使得错误类型更简单。此外,compound-error
尽可能保持瘦小,以便于开始。这就是 compound-error
存在的原因;使事情变得简单!
有哪些替代方案,为什么 compound-error
更好?
- error-chain - 虽然功能强大,但入门时有点难以使用。此外,其语法并不完全符合 Rust。它引入了一些类似于函数的宏,定义了自己的语法。
compound-error
限制自己仅使用 derive 宏和 derive 宏辅助属性,这是生成错误类型 impl 所需的最小内容。 - quick-error - 错误指定很容易,但错误指定也使用类似于函数的宏进行,引入了自己的语法。
- failure - 它非常复杂。
compound-error
尝试尽可能简单。 - composite-error - 没有实现
std::error::Error
。compound-error
自动实现了std::error::Error
并提供在需要时跳过自动实现的方法。
此外,这些包中没有任何一个声称有目标保持错误层次结构扁平。这是 compound-error
包的主要贡献。
依赖关系
~1.5MB
~36K SLoC