#error #error-type #enums #type #macro

compound-error

扁平层次结构的复合错误

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

Apache-2.0

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::Errorcompound-error 自动实现了 std::error::Error 并提供在需要时跳过自动实现的方法。

此外,这些包中没有任何一个声称有目标保持错误层次结构扁平。这是 compound-error 包的主要贡献。

依赖关系

~1.5MB
~36K SLoC