《Effective Rust》第 4 条:优先使用惯用的错误类型

  • King
  • 更新于 2024-06-15 09:31
  • 阅读 849

第4条:优先使用惯用的错误类型第3条描述了如何使用标准库为Option和Result类型提供的转换,以允许使用?运算符简洁、惯用地处理结果类型。但它没有讨论如何最好地处理作为Result<T,E>第二个类型参数出现的各种不同的错误类型E;这就是本章节的内容。只有当有多

第 4 条:优先使用惯用的错误类型

[第 3 条]描述了如何使用标准库为 OptionResult 类型提供的转换,以允许使用 ? 运算符简洁、惯用地处理结果类型。但它没有讨论如何最好地处理作为 Result&lt;T, E> 第二个类型参数出现的各种不同的错误类型 E;这就是本章节的内容。

只有当有多种不同的错误类型时,这才有相关性。如果函数遇到的所有不同错误已经是同一类型,它可以只返回该类型。当有不同类型的错误时,需要做出一个决定,即是否保留子错误类型信息。

错误特征(Error Trait)

了解标准特征([第 10 条])总是一个好主意,这里相关的特征是 std::error::ErrorResultE 类型参数不必是实现 Error 的类型,但这是一个常见的约定,它允许包装器表达适当的特征约束 —— 因此,最好为您的错误类型实现 Error

首先要注意的是,对于错误类型,唯一硬性要求是特征约束:实现 Error 的任何类型也必须实现以下特征:

  • Display 特征,意味着可以使用 {} 进行格式化

  • Debug 特征,意味着可以使用 {:?} 进行格式化

换句话说,应该能够将错误类型显示给用户和程序员。

特征中唯一的方法是 source(),[^1] 它允许错误类型公开一个内部的、嵌套的错误。此方法是可选的 —— 它带有一个返回 None 的默认实现([第 13 条]),表示内部错误信息不可用。 最后要注意的一点是:如果您正在为 no_std 环境([第 33 条])编写代码,可能无法实现 Error —— Error 特征目前在 std 中实现,而不是 core,因此不可用。[^2]

最小错误(Minimal Errors)

如果不需要嵌套错误信息,那么错误类型的实现不必比 String 复杂多少 —— 这是一个“字符串类型”的变量可能合适的罕见情况。但它需要比 String 多一点;虽然可以使用 String 作为 E 类型参数:

pub fn find_user(username: &str) -> Result&lt;UserId, String> {
    let f = std::fs::File::open("/etc/passwd")
        .map_err(|e| format!("Failed to open password file: {:?}", e))?;
    // ...
}

一个 String 并不实现 Error,我们希望是这样,以便代码的其他部分可以处理 Errors。为 String 实现 Error 是不可能的,因为特征(trait)和类型都不属于我们(所谓的孤儿规则):

impl std::error::Error for String {}
error[E0117]: only traits defined in the current crate can be implemented for
              types defined outside of the crate
  --> src/main.rs:18:5
   |
18 |     impl std::error::Error for String {}
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^------
   |     |                          |
   |     |                          `String` is not defined in the current crate
   |     impl doesn't use only types from inside the current crate
   |
   = note: define and implement a trait or new type instead

[类型别名]也无济于事,因为它并没有创建一个新的类型,所以也不会改变错误信息:

pub type MyError = String;

impl std::error::Error for MyError {}
error[E0117]: only traits defined in the current crate can be implemented for
              types defined outside of the crate
  --> src/main.rs:41:5
   |
41 |     impl std::error::Error for MyError {}
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^-------
   |     |                          |
   |     |                          `String` is not defined in the current crate
   |     impl doesn't use only types from inside the current crate
   |
   = note: define and implement a trait or new type instead

像往常一样,编译器错误消息为解决问题提供了一个线索。定义一个包装 String 类型的元组结构体("新类型模式",[第 6 条])允许实现 Error 特征,前提是也实现了 DebugDisplay

#[derive(Debug)]
pub struct MyError(String);

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter&lt;'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl std::error::Error for MyError {}

pub fn find_user(username: &str) -> Result&lt;UserId, MyError> {
    let f = std::fs::File::open("/etc/passwd").map_err(|e| {
        MyError(format!("Failed to open password file: {:?}", e))
    })?;
    // ...
}

为了方便起见,实现 From&lt;String> 特征可能是有意义的,以便可以轻松地将字符串值转换为 MyError 实例([第 5 条]):

impl From&lt;String> for MyError {
    fn from(msg: String) -> Self {
        Self(msg)
    }
}

当编译器遇到问号运算符(?)时,它会自动应用任何需要的 From 特征实现,以便达到目标错误返回类型。这允许进一步的最小化:

pub fn find_user(username: &str) -> Result&lt;UserId, MyError> {
    let f = std::fs::File::open("/etc/passwd")
        .map_err(|e| format!("Failed to open password file: {:?}", e))?;
    // ...
}

这里的错误路径涵盖了以下步骤:

  • File::open 返回一个类型为 std::io::Error 的错误。
  • format! 使用 std::io::ErrorDebug 实现将其转换为 String
  • ? 使编译器寻找并使用一个 From 实现,该实现可以将它从 String 转换为 MyError

嵌套错误

另一种情况是,嵌套错误的内容重要到足以需要被保留并供调用者使用。

考虑一个库函数,它尝试返回文件的第一行作为字符串,只要这一行不是太长。稍微思考一下就会发现(至少)三种可能发生的不同类型的失败:

  • 文件可能不存在或者无法读取。
  • 文件可能包含不是有效 UTF-8 的数据,因此无法转换为 String
  • 文件可能有一个过长的一行。

根据 [第 1 条],您可以使用类型系统来表达并包含所有这些可能性作为一个枚举

#[derive(Debug)]
pub enum MyError {
    Io(std::io::Error),
    Utf8(std::string::FromUtf8Error),
    General(String),
}

这个枚举定义包括了 derive(Debug),但为了满足 Error 特征,还需要一个 Display s实现:


impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::F...

剩余50%的内容订阅专栏后可查看

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
King
King
0x56af...a0dd
擅长Rust/Solidity/FunC/Move开发