我发现自己的函数使用了不同类型的错误,其中一些来自
std
(或任何地方),其中一些是我的。我想使用 ?
运算符来减少一般的开销,但是当我有不同的类型时,这很棘手。
在下面的代码中,
foo
可能会因std::io::Error
错误或MyError
而失败。
use std::fs::File;
pub enum MyError {
IO(std::io::Error),
Custom(String),
}
impl From<std::io::Error> for MyError {
fn from(value: std::io::Error) -> Self {
MyError::IO(value)
}
}
fn bar(fail: bool) -> Result<usize, MyError> {
if fail {
Err(MyError::Custom("Was told to fail".to_owned()))
} else {
Ok(42)
}
}
pub fn foo(path: &str, fail: bool) -> Result<usize, MyError> {
let file: File = File::open(path)?;
let b: usize = bar(fail)?;
Ok(b)
}
我可以通过让
MyError
成为一个带有包裹 std::io::Error
的分支的枚举来使其工作,但我不喜欢这种方法。我还尝试使 MyError
成为具有 std::io::Error
实现的特征,但这不起作用,因为返回类型是动态的,我必须搞乱装箱/拆箱。
我不喜欢我的解决方案有几个原因,其中最重要的是,随着更多错误类型的出现,它的扩展性似乎会很差。例如,假设我还有可以抛出
std::fmt::Error
的函数,可能与其他两个组合。如果我只是将其添加到 MyError
中,则调用 foo
并尝试解决错误的函数看起来 foo
可能会因 fmt
错误而失败,但事实并非如此。但是,如果我为每种组合制作不同版本的 MyError
,情况会更糟 - 我需要针对每种错误组合使用不同的 MyError
实现。
(最重要的是,由于额外的包装,它看起来很笨重,而且事实上,它是针对似乎常见问题的手动实现。)
有没有更好的方法来解决这个问题?我真的想说,
foo
可能会失败,其结果可能是一组静态已知的不同错误之一,理想情况下同时保持简洁的 ?
语法。
我看到了 合并两种错误类型最惯用的方法是什么?,这似乎并不令人鼓舞,但我的问题有点具体,我很好奇在接下来的几年里是否出现了更好的解决方案。
一般有两种方法可以解决这个问题。
enum
Error
特质抽象enum
方式优点是可以保留类型信息;缺点是您需要手动指定每种可能的类型。
这是您目前拥有的解决方案。虽然我不建议手动实现其特征,但我宁愿推荐或多或少的非官方标准板条箱
thiserror
:
use thiserror::Error;
#[derive(Error, Debug)]
pub enum MyError {
#[error("IO Error")]
IO(#[from] std::io::Error),
#[error("Other Error")]
Custom(String),
}
注意
#[from]
标签,它将自动为您生成 From
impl。
Error
特质抽象方式每个错误都应该实现
Error
特征;如果是这样,有几种方法可以抽象它们。
内置方式是
Box<dyn Error>
,尽管它有一些缺点。最值得注意的是,它只允许传递错误,但在许多情况下,您希望在错误通过调用堆栈时添加更多信息。此外,您还会丢失有关错误实际类型的所有信息。因此,这种方法主要推荐用于快速原型设计,它可能不是生产的正确方法。
这是
Box<dyn Error>
版本的外观:
use std::{error::Error, fs::File};
fn bar(fail: bool) -> Result<usize, String> {
if fail {
Err("Was told to fail".to_owned())
} else {
Ok(42)
}
}
pub fn foo(path: &str, fail: bool) -> Result<usize, Box<dyn Error>> {
let file: File = File::open(path)?;
let b: usize = bar(fail)?;
Ok(b)
}
anyhow
或 miette
。
这是一个
anyhow
解决方案:
use anyhow::{anyhow, Result};
use std::fs::File;
fn bar(fail: bool) -> Result<usize> {
if fail {
Err(anyhow!("Was told to fail"))
} else {
Ok(42)
}
}
pub fn foo(path: &str, fail: bool) -> Result<usize> {
let file: File = File::open(path)?;
let b: usize = bar(fail)?;
Ok(b)
}
就我个人而言,在我的项目中,通常会结合使用
thiserror
和 miette
。这允许您在需要知道确切类型时使用 enums
,但也允许您使用 Error
类型进行抽象。
我真的很喜欢
miette
,因为有了 fancy
功能,它可以为您提供非常好的分层错误消息。
这里有两种方法可以实现您在
miette
中的情况:
miette
的enum
方式:use std::fs::File;
use thiserror::Error;
use miette::{Diagnostic, Result};
#[derive(Error, Diagnostic, Debug)]
enum MyError {
#[error("IO Error")]
IO(#[from] std::io::Error),
#[error("Other Error")]
Custom(String),
}
fn bar(fail: bool) -> Result<usize, MyError> {
if fail {
Err(MyError::Custom("Was told to fail".to_string()))
} else {
Ok(42)
}
}
fn foo(path: &str, fail: bool) -> Result<usize, MyError> {
let file: File = File::open(path)?;
let b: usize = bar(fail)?;
Ok(b)
}
fn main() -> Result<()> {
let value = foo("does_not_exist.txt", false)?;
println!("{}", value);
Ok(())
}
Error: × IO Error
╰─▶ The system cannot find the file specified. (os error 2)
miette
的Error
抽象方式:use std::fs::File;
use miette::{bail, Context, IntoDiagnostic, Result};
fn bar(fail: bool) -> Result<usize> {
if fail {
bail!("Was told to fail")
}
Ok(42)
}
fn foo(path: &str, fail: bool) -> Result<usize> {
let _file: File = File::open(path)
.into_diagnostic()
.wrap_err("Opening file failed")?;
let b: usize = bar(fail)?;
Ok(b)
}
fn main() -> Result<()> {
let value = foo("does_not_exist.txt", false)?;
println!("{}", value);
Ok(())
}
Error: × Opening file failed
╰─▶ The system cannot find the file specified. (os error 2)
您选择哪一个当然是个人最喜欢的。