改进rust代码的35种具体方法-类型(四)

本文探讨了如何在Rust中处理Result类型中的多种错误类型,包括实现Errortrait的重要性,以及使用枚举、trait对象和库与应用代码中错误处理的不同策略。推荐使用thiserrorcrate简化错误类型定义。
摘要由CSDN通过智能技术生成

接上篇的文章

上篇描述了如何使用标准库为OptionResult类型提供的转换,以允许使用?对结果类型进行简洁、惯用的处理。操作员。它没有讨论如何最好地处理作为Result<T, E>的第二个类型参数出现的各种不同错误类型E;这就是本项目的主题。

只有当游戏中存在各种不同的错误类型时,这才真正相关;如果函数遇到的所有不同错误都已经属于同一类型,它只能返回该类型。当存在不同类型的错误时,需要决定是否应该保留子错误类型信息。

Error特征

了解标准特征包括什么,以及这里的相关特征是std::error::Error,这总是好的。ResultE类型参数不一定是实现Error的类型,但它是一个常见的惯例,允许包装器表达适当的特征边界-因此更喜欢为您的错误类型实现Error。但是,如果您正在为no_std环境编写代码,此建议更难应用——Error特征目前在std中实现,而不是core,因此不可用。

首先需要注意的是,Error类型的唯一硬要求是特征边界:任何实现Error类型也必须同时实现:

  • Display特征,这意味着它可以format!ed与{},和
  • Debug特征,这意味着它可以format!ed与{:?}

换句话说,应该可以向用户和程序员显示Error类型。

特征中的only2方法是source()它允许Error类型公开一个内部嵌套的错误。此方法是可选的——它带有默认实现返回None,表示内部错误信息不可用。

最小错误

如果不需要嵌套错误信息,那么Error类型的实现不需要超过String——在极少数情况下,“字符串类型”变量可能是合适的。不过,它确实需要比String一点;虽然可以使用String作为E类型参数:

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

String不会实现Error,我们更喜欢错误,这样代码的其他区域就可以处理Error。不可能为Stringimpl Error,因为特征和类型都不属于我们(所谓的孤儿规则):

    impl std::error::Error for String {}
error[E0117]: only traits defined in the current crate can be implemented for arbitrary types
  --> errors/src/main.rs:20:5
   |
20 |     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 arbitrary types
  --> errors/src/main.rs:43:5
   |
43 |     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类型的元组结构(“新类型模式”,)允许实现Error特征,前提是DebugDisplay也已实现:

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

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

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

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

为了方便起见,实现From<String>特征以允许字符串值轻松转换为MyError实例可能是有意义的:

    impl std::convert::From<String> for MyError {
        fn from(msg: String) -> Self {
            Self(msg)
        }
    }

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

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

对于这里的错误路径:

  • File::open返回类型为接上篇的文章的错误。
  • format!使用std::io::ErrorDebug实现将其转换为String
  • ?使编译器查找并使用From实现,该实现可以将其从String变为MyError

嵌套错误

另一种情况是,嵌套错误的内容足够重要,应该保留并向调用者提供。

考虑一个库函数,该函数试图将文件的第一行作为字符串返回,只要该行不是太长。片刻的思考揭示了(至少)可能发生的三种不同类型的失败:

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

根据之前枚举,您可以使用类型系统作为enum来表达和包含所有这些可能性:

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

enum定义包括derive(Debug)但为了满足Error特征,还需要Display实现。

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            MyError::Io(e) => write!(f, "IO error: {}", e),
            MyError::Utf8(e) => write!(f, "UTF-8 error: {}", e),
            MyError::General(s) => write!(f, "General error: {}", s),
        }
    }
}

覆盖默认的source()实现以方便访问嵌套错误也是有意义的。

use std::error::Error;

impl Error for MyError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            MyError::Io(e) => Some(e),
            MyError::Utf8(e) => Some(e),
            MyError::General(_) => None,
        }
    }
}

这允许错误处理简洁,同时仍然保留不同错误类别的所有类型信息:

    /// Return the first line of the given file.
    pub fn first_line(filename: &str) -> Result<String, MyError> {
        let file = std::fs::File::open(filename).map_err(MyError::Io)?;
        let mut reader = std::io::BufReader::new(file);

        // (A real implementation could just use `reader.read_line()`)
        let mut buf = vec![];
        let len = reader.read_until(b'\n', &mut buf).map_err(MyError::Io)?;
        let result = String::from_utf8(buf).map_err(MyError::Utf8)?;
        if result.len() > MAX_LEN {
            return Err(MyError::General(format!("Line too long: {}", len)));
        }
        Ok(result)
    }

为所有子错误类型实现From特征也是一个好主意:

impl From<std::io::Error> for MyError {
    fn from(e: std::io::Error) -> Self {
        Self::Io(e)
    }
}
impl From<std::string::FromUtf8Error> for MyError {
    fn from(e: std::string::FromUtf8Error) -> Self {
        Self::Utf8(e)
    }
}

这可以防止图书馆用户自己在孤儿规则下遭受痛苦:他们不允许在MyError上实现From,因为特征和结构都是他们外部的。

更好的是,实现From可以更加精确,因为三目运算符将自动执行任何必要的From转换:

    /// Return the first line of the given file.
    pub fn first_line(filename: &str) -> Result<String, MyError> {
        let file = std::fs::File::open(filename)?; // via `From<std::io::Error>`
        let mut reader = std::io::BufReader::new(file);
        let mut buf = vec![];
        let len = reader.read_until(b'\n', &mut buf)?; // via `From<std::io::Error>`
        let result = String::from_utf8(buf)?; // via `From<std::string::FromUtf8Error>`
        if result.len() > MAX_LEN {
            return Err(MyError::General(format!("Line too long: {}", len)));
        }
        Ok(result)
    }

编写完整的错误类型可能涉及相当多的样板文件;考虑使用thiserror crate来帮助解决这个问题,因为它在不添加额外的运行时依赖项的情况下减少了工作量。

特质对象

嵌套错误的第一种方法抛弃了所有子错误细节,只是保留了一些字符串输出(format!("{:?}", err))。第二种方法保留了所有可能的子错误的完整类型信息,但需要对所有可能的子错误类型进行完整枚举。

这就提出了一个问题:这两种方法之间是否有半路,保留子错误信息,而不需要手动包含所有可能的错误类型?

将子错误信息编码为特征对象可以避免各种可能性的enum变体,但会擦除特定底层错误类型的细节。此类对象的接收者将有权访问Error特征的方法——display()debug()source())——但不知道子错误的原始静态类型。

#[derive(Debug)]
pub enum WrappedError {
    Wrapped(Box<dyn Error>),
    General(String),
}

impl std::fmt::Display for WrappedError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Wrapped(e) => write!(f, "Inner error: {}", e),
            Self::General(s) => write!(f, "{}", s),
        }
    }
}

事实证明,这是可能的,但它出乎意料地微妙。部分困难来自对特征对象的对象安全约束(项目12),但Rust的一致性规则也发挥作用,这(大致)表明,对于一个类型,最多只能实现一个特征。

假定的WrappedError会天真地实现Error特征,并实现From<Error>特征,以允许子错误轻松包装。这意味着aWrappedErrorfrom内部的WrappedError创建,因为WrappedError实现了Error,并且与From的全面反射实现相冲突:

error[E0119]: conflicting implementations of trait `std::convert::From<WrappedError>` for type `WrappedError`
   --> errors/src/main.rs:253:1
    |
253 | impl<E: 'static + Error> From<E> for WrappedError {
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |
    = note: conflicting implementation in crate `core`:
            - impl<T> From<T> for T;

库与应用程序

上一节的最终建议包括“...在应用程序中处理错误”的资格。这是因为为库中重复使用而编写的代码和构成顶级应用程序的代码之间经常存在区别。

为库编写的代码无法预测使用代码的环境,因此最好发出具体、详细的错误信息,并让调用者弄清楚如何使用该信息。这倾向于前面描述的enum式嵌套错误(也避免了对库公共API的依赖)。

然而,应用程序代码通常需要更多地关注如何向用户呈现错误。它还可能必须应对其依赖图中存在的所有库发出的所有不同错误类型。因此,更动态的错误类型(如asanyhowanyhow::Error)使整个应用程序的错误处理更简单、更一致。

总结

这个项目涵盖了很多领域,所以有一个总结:

  • 标准Error特征不需要您,因此更愿意为您的错误类型实现它。
  • 在处理异构底层错误类型时,请决定是否需要保留这些类型。
    • 如果没有,请考虑anyhow在应用程序代码中包装子错误。
    • 如果需要,请将它们编码在枚举enum并提供转换。考虑使用thiserror来帮助解决这个问题。
  • anyhow考虑使用板条箱进行方便、惯用的错误处理。

这是你的决定,但无论你决定什么,请在类型系统中进行编码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值