上篇描述了如何使用标准库为Option
和Result
类型提供的转换,以允许使用?
对结果类型进行简洁、惯用的处理。操作员。它没有讨论如何最好地处理作为Result<T, E>
的第二个类型参数出现的各种不同错误类型E
;这就是本项目的主题。
只有当游戏中存在各种不同的错误类型时,这才真正相关;如果函数遇到的所有不同错误都已经属于同一类型,它只能返回该类型。当存在不同类型的错误时,需要决定是否应该保留子错误类型信息。
Error特征
了解标准特征包括什么,以及这里的相关特征是std::error::Error,这总是好的。Result
的E
类型参数不一定是实现Error
的类型,但它是一个常见的惯例,允许包装器表达适当的特征边界-因此更喜欢为您的错误类型实现Error
。但是,如果您正在为no_std
环境编写代码,此建议更难应用——Error
特征目前在std
中实现,而不是core
,因此不可用。
首先需要注意的是,Error
类型的唯一硬要求是特征边界:任何实现Error
类型也必须同时实现:
换句话说,应该可以向用户和程序员显示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
。不可能为String
impl 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
特征,前提是Debug
和Display
也已实现:
#[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::Error
的Debug
实现将其转换为String
。?
使编译器查找并使用From
实现,该实现可以将其从String
变为MyError
。
嵌套错误
另一种情况是,嵌套错误的内容足够重要,应该保留并向调用者提供。
考虑一个库函数,该函数试图将文件的第一行作为字符串返回,只要该行不是太长。片刻的思考揭示了(至少)可能发生的三种不同类型的失败:
根据之前枚举,您可以使用类型系统作为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
考虑使用板条箱进行方便、惯用的错误处理。
这是你的决定,但无论你决定什么,请在类型系统中进行编码