rust 源错误是否应该在显示输出中包含该源?

jjjwad0x  于 2023-04-30  发布在  其他
关注(0)|答案(1)|浏览(159)

我有一个错误类型,它实现了Error trait,并且它 Package 了一个潜在的错误原因,因此source方法返回Some(source)。我想知道我的错误类型上的Display impl是否应该包括源错误的描述。
我可以看到两种选择:
1.是,在Display输出中包含source,e.g.“打开数据库时出错:无此文件”
这使得只通过使用"{}"化就可以轻松地打印整个错误链,但不可能只显示错误本身而不显示源错误的基础链。此外,它使source方法有点无意义,并且使客户端代码无法选择如何格式化链中每个错误之间的分隔。然而,在我找到的示例代码中,这种选择似乎很常见。
1.不,只打印错误本身e。g.“打开数据库时出错”,如果客户端代码希望在输出中包含source,则将其留给客户端代码来遍历和显示。
这使客户端代码可以选择是只显示表面错误还是整个链,以及在后一种情况下如何格式化链中每个错误之间的分隔。它让客户端代码承担了遍历错误链的负担,我还没有找到一个规范的实用程序,可以方便地格式化错误链,每个错误都只有Display本身,不包括source。(当然,我也有自己的。)
snafu crate(我真的很喜欢)似乎暗示支持选项2,因为一个带有source字段但没有display属性的错误变体默认为格式化Display输出,而不包含source
也许我真实的的问题是source方法的目的是什么?是为了使错误链的格式更灵活吗?或者Display真的应该输出所有用户可见的错误,而source只是为了开发人员可见的目的?
我希望看到一些关于这方面的明确指导,最好是在Error trait的文档中。

#[derive(Debug)]
enum DatabaseError {
    Opening { source: io::Error },
}

impl Error for DatabaseError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            DataBaseError::Opening { source } => Some(source),
        }
    }
}

impl fmt::Display for DatabaseError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            DatabaseError::Opening { source } => {
                // ??? Should we include the source?
                write!(f, "Error opening database: {}", source)

                // ??? Or should we leave it to the caller to call .source()
                //     if they want to include that in the error description?
                write!(f, "Error opening database")
            }
        }
    }
}
5sxhfpxr

5sxhfpxr1#

tl;dr-要么在源链中包含底层错误,要么在Display上追加源消息,但 * 不能两者都包含 *。您的里程数可能会有所不同。

是否在Display实现上打印源错误的两个选项产生了两种设计流派。这个答案将解释两者,同时客观地说明它们的关键区别,并澄清一些可能的误解。

设计一:是,在Display实施中包括source

SNAFU示例:

#[derive(Debug, Snafu)]
enum Error {
    #[snafu(display("Could not read data set token: {}", source))]
    ReadToken {
        #[snafu(backtrace)]
        source: ReadDataSetError,
    },
}

正如问题中已经提到的,关键的优点是提供全部信息就像打印错误值一样简单。

eprintln!("[ERROR] {}", err);

它简单易用,不需要帮助函数来报告错误,尽管缺乏表示的灵活性。如果不进行字符串操作,您将始终得到一连串冒号分隔的错误。

[ERROR] Could not read data set token: Could not read item value: Undefined value length of element tagged (5533,5533) at position 3548

设计二:否,请在Display实施中遗漏source

#[derive(Debug, Snafu)]
enum Error {
    #[snafu(display("Could not read data set token"))]
    ReadToken {
        #[snafu(backtrace)]
        source: ReadDataSetError,
    },
}

虽然这不会像以前那样通过一行打印提供完整的信息,但您可以将该任务留给项目范围的错误报告程序。这也为API的使用者提供了更大的错误表示灵活性。
下面是一个简单的例子。将需要额外的逻辑来呈现错误的回溯。

fn report<E: 'static>(err: E)
where
    E: std::error::Error,
    E: Send + Sync,
{
    eprintln!("[ERROR] {}", err);
    if let Some(cause) = err.source() {
        eprintln!();
        eprintln!("Caused by:");
        for (i, e) in std::iter::successors(Some(cause), |e| e.source()).enumerate() {
            eprintln!("   {}: {}", i, e);
        }
    }
}

Playground
与固执己见的库集成的兴趣也值得考虑。也就是说,生态系统中的某些板条箱可能已经做出了选择的假设。在anyhow中,默认情况下错误报告已经遍历了错误的源链。当使用anyhow进行错误报告时,你不应该添加source,否则你可能会得到一个令人恼火的重复消息列表:

[ERROR] Could not read data set token: Could not read item value: Undefined value length of element tagged (5533,5533) at position 3548

Caused by:
   0: Could not read item value: Undefined value length of element tagged (5533,5533) at position 3548
   1: Undefined value length of element tagged (5533,5533) at position 3548

同样,eyre库提供了一个可定制的错误报告抽象,但是eyre crate生态系统中现有的错误报告器也假设错误的源代码不是由Display实现打印的。

那么,是哪一个?

在错误处理项目组的努力下,2021年初提出了关于Display实施的关键指南:
带有源错误的错误类型应该通过source返回该错误,或者在其自己的Display输出中包含该源的错误消息,但决不能同时返回这两个错误。
这是第二个设计**:避免在Display实现中附加源代码的错误消息。SNAFU用户可以使用其Report API(从版本0开始提供)。7.2),或引入自定义错误报告器。由于生态系统尚未围绕这一准则成熟,人们可能仍然会发现错误实用工具箱缺乏对这种方式的错误报告的支持。

无论哪种情况。..

该决定仅在错误报告中起作用,而不是以某种其他方式在错误匹配或错误处理中起作用。source方法的存在在所有错误类型上建立了一个链式 * 结构 *,可以在模式匹配和程序的后续流控制中利用它。Error::source方法在生态系统中有其用途,无论如何报告错误。
此外,最终由开发人员选择如何设计他们的错误和相应的Display实现,尽管一旦它开始与其他组件集成,遵循指南将是实现一致错误报告的正确方法。

Rust API指南如何?

Rust API指南并没有对错误中的Display提出意见,除了C-GOOD-ERR,它只规定错误类型的Display消息应该是 “小写,没有尾随标点符号,通常简洁”。有一个pending proposal来更新这个指南,指示开发人员在他们的Display impl中排除source。但是,pull request是在指南提出之前创建的,并且从那时起(在撰写本文时)尚未更新。
参见:

相关问题