我有一个错误类型,它包含 Error
特征,它包装了一个潜在的错误原因,所以 source
方法返回 Some(source)
。我想知道我的错误类型上的 Display
impl 是否应该包含对该源错误的描述。
我可以看到两个选项:
source
输出中包含 Display
,例如“打开数据库时出错:没有这样的文件” 这使得仅通过使用
"{}"
格式化就可以轻松打印整个错误链,但不可能只显示错误本身而没有底层的源错误链。此外,它使 source
方法有点毫无意义,并且让客户端代码无法选择如何格式化链中每个错误之间的格式。尽管如此,在我发现的示例代码中,这种选择似乎很常见。source
如果它想将其包含在输出中。 这使客户端代码可以选择是只显示表面错误还是整个链,以及在后一种情况下如何格式化链中每个错误之间的分隔。它让客户端代码承担了遍历链的负担,我还没有使用规范实用程序来方便地从错误中格式化错误链,每个错误都只有
Display
本身,不包括 source
。 (所以我当然有自己的。)snafu crate(我真的很喜欢)似乎暗示支持选项 2,因为带有
source
字段但没有 display
属性的错误变体默认格式化不包含 Display
的 source
输出。也许我真正的问题是:
source
方法的目的是什么?是为了让格式化错误链更加灵活吗?或者 Display
真的应该输出关于错误的所有用户可见的内容,而 source
只是为了开发人员可见的目的?我很想看到一些明确的指导,最好是在
Error
特性的文档中。#[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")
}
}
}
}
最佳答案
是否在 Display
实现上打印源错误的两个选项创建了两种设计流派。
目前,尽管存在对这两种方式的看法,但两者都没有比另一个更惯用,并且最终可以在 future 找到社区范围的共识,或者在特定项目或代码库的上下文中可能已经如此。
除了 C-GOOD-ERR 之外,Rust API 指南没有对错误中的 Display
提出意见,该指南仅指出错误类型的 Display
消息应该是“小写,没有尾随标点符号,并且通常简洁”。有一个 pending proposal 来更新这个指南,指示开发人员在他们的 source
impl 中排除 Display
。同样,如果没有一些摩擦,该提案就不会存在。
在这里和现在可以做的是客观地陈述两者之间的主要区别,同时澄清一些可能的误解。
1. 是的,在你的 source
impl 中包含 Display
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
2. 不,在你的 source
impl 上省略 Display
#[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);
}
}
}
还值得考虑与固执己见的库集成的兴趣。也就是说,生态系统中的某些 crate 可能已经对选择哪个选项做出了假设。在 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
实现打印的。在任一情况下...
这个决定只在错误报告中起作用,在错误匹配或以其他方式处理错误时不起作用。
source
方法的存在为所有错误类型建立了一个链状结构,可以在程序的模式匹配和后续流程控制中加以利用。Error::source
方法在生态系统中是有目的的,不管错误是如何报告的。也可以看看:
关于error-handling - 源错误是否应该在显示输出中包含该源?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62869360/