在这篇博文中,我们将探索在Rust中使用两个流行的库来简化错误处理的策略:thiserror和anyway。我们将讨论它们的特性、用例,并提供关于何时选择每个库的见解。
需求提出
让我们首先创建函数decode()来进行说明。该功能有3个步骤:
- 从名为input的文件中读取内容
- 将每行解码为base64字符串
- 输出打印解码后的字符串
挑战在于确定decode的返回类型,因为std::fs::read_to_string() 、base64 decode() 和String::from_utf8() 各自返回不同的错误类型。
use base64::{self, engine, Engine};
fn decode() -> /* ? */ {
let input = std::fs::read_to_string("input")?;
for line in input.lines() {
let bytes = engine::general_purpose::STANDARD.decode(line)?;
println!("{}", String::from_utf8(bytes)?);
}
Ok(())
}
应对方法是使用trait object: Box。这是可行的,因为所有类型都实现了std::error::Error。
fn decode() -> Result<(), Box<dyn std::error::Error>> {
// ...
}
虽然这在某些情况下是合适的,但它限制了调用者识别decode()中发生的实际错误的能力。然后,如果希望以不同的方式处理具体错误,则需要使用enum定义错误类型:
enum AppError {
ReadError(std::io::Error),
DecodeError(base64::DecodeError),
StringError(std::string::FromUtf8Error),
}
通过实现std::error::Error trait,我们可以在语义上将AppError标记为错误类型。
impl std::error::Error for AppError {}
然而,这段代码无法编译,因为AppError不满足std::error::Error需要Display和Debug的约束:
error[E0277]: `AppError` doesn't implement `std::fmt::Display`
error[E0277]: `AppError` doesn't implement `Debug`
std::error::Error的定义代表了Rust中对错误类型的最低要求的共识。错误应该对用户(显示)和程序员(调试)有两种形式的描述,并且应该提供其最终错误原因。
pub trait Error: Debug + Display {
fn source(&self) -> Option<&(dyn Error + 'static)> { ... }
// ...
}
在实现所需的特征后,代码将是这样的:
impl std::error::Error for AppError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
use AppError::*;
match self {
ReadError(e) => Some(e),
DecodeError(e) => Some(e),
StringError(e) => Some(e),
}
}
}
impl std::fmt::Display for AppError { // Error message for users.
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use AppError::*;
let message = match self {
ReadError(_) => "Failed to read the file.",
DecodeError(_) => "Failed to decode the input.",
StringError(_) => "Failed to parse the decoded bytes.",
};
write!(f, "{message}")
}
}
impl std::fmt::Debug for AppError { // Error message for programmers.
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "{self}")?;
if let Some(e) = self.source() { // <-- Use source() to retrive the root cause.
writeln!(f, "\tCaused by: {e:?}")?;
}
Ok(())
}
}
到现在可以在decode()中使用AppError:
fn decode() -> Result<(), AppError> {
let input = std::fs::read_to_string("input").map_err(AppError::ReadError)?;
// ...
map_err() 用于将std::io::Error转换为AppError::ReadError。使用?操作符为了更好的流程,我们可以为AppError实现From trait:
impl From<std::io::Error> for AppError {
fn from(value: std::io::Error) -> Self {
AppError::ReadError(value)
}
}
impl From<base64::DecodeError> for AppError {
fn from(value: base64::DecodeError) -> Self {
AppError::DecodeError(value)
}
}
impl From<std::string::FromUtf8Error> for AppError {
fn from(value: std::string::FromUtf8Error) -> Self {
AppError::StringError(value)
}
}
fn decode() -> Result<(), AppError> {
let input = std::fs::read_to_string("input")?;
for line in input.lines() {
let bytes = engine::general_purpose::STANDARD.decode(line)?;
println!("{}", String::from_utf8(bytes)?);
}
Ok(())
}
fn main() {
if let Err(error) = decode() {
println!("{error:?}");
}
}
我们做了几件事来流畅地使用自定义错误类型:
- 实现std::error::error
- 实现Debug和Display
- 实现From
上面代码实现有点冗长而乏味,但幸运的是,thiserror会自动生成其中的大部分。
thiserror简化错误定义
下面使用thiserror包简化上面代码:
#[derive(thiserror::Error)]
enum AppError {
#[error("Failed to read the file.")]
ReadError(#[from] std::io::Error),
#[error("Failed to decode the input.")]
DecodeError(#[from] base64::DecodeError),
#[error("Failed to parse the decoded bytes.")]
StringError(#[from] std::string::FromUtf8Error),
}
impl std::fmt::Debug for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "{self}")?;
if let Some(e) = self.source() {
writeln!(f, "\tCaused by: {e:?}")?;
}
Ok(())
}
}
#[error]宏生成Display, #[from]宏处理from实现source()转换std::error::error。Debug的实现仍然是提供详细的错误消息,但如果够用的话,也可以使用#derive[Debug]:
// The manual implementation of Debug
Failed to decode the input.
Caused by: InvalidPadding
// #[derive(Debug)]
DecodeError(InvalidPadding)
anyhow处理任何错误
在 Rust 中,anyhow
是一个用于方便地处理错误的库。它提供了一种简单的方式来处理各种类型的错误,将不同的错误类型统一转换为anyhow::Error
类型,使得错误处理更加灵活和简洁。anyhow
构建在std::error::Error
的基础上,允许在函数之间轻松地传播错误,而不需要在每个函数签名中指定具体的错误类型。
anyhow提供了简化错误处理的替代方法,类似于Box方法,下面是上面示例的再次实现:
fn decode() -> Result<(), anyhow::Error> {
let input = std::fs::read_to_string("input")?;
for line in input.lines() {
let bytes = engine::general_purpose::STANDARD.decode(line)?;
println!("{}", String::from_utf8(bytes)?);
}
Ok(())
}
它可以编译,因为实现std::error:: error的类型可以转换为anyway::error。错误消息如下:
Invalid padding
为了输出更多错误消息,可以使用contex():
let bytes = engine::general_purpose::STANDARD
.decode(line)
.context("Failed to decode the input")?;
现在错误消息为:
Failed to decode the input
Caused by:
Invalid padding
现在,由于anyway的类型转换和context(),我们的错误处理得到了简化。
异步编程anyhow应用
- 在异步编程中,
anyhow
也可以很好地用于处理异步操作可能出现的错误。 - 例如,一个异步函数用于从网络获取数据并进行处理,可能会遇到网络请求失败、数据解析错误等情况。
- 以下是示例代码(假设使用
tokio
进行异步编程):
use anyhow::{anyhow, Result};
use tokio::io::AsyncReadExt;
use tokio::net::TcpStream;
async fn get_and_process_data() -> Result<String> {
let mut stream = TcpStream::connect("127.0.0.1:8080").await?;
let mut buffer = [0; 1024];
let n = stream.read(&mut buffer).await?;
let data = String::from_utf8_lossy(&buffer[..n]);
// 假设这里有一个简单的处理逻辑,可能会出错
if data.is_empty() {
return Err(anyhow!("Received empty data"));
}
Ok(data.into_owned())
}
在这个异步函数中,TcpStream::connect
和stream.read
可能会返回错误,通过?
操作符可以方便地将anyhow::Error
类型的错误向上传播。
最后总结
总之,我们已经探索了thiserror 和 anyhow库的独特特性,并讨论了每个库的优点。通过选择合适的工具,Rust开发人员可以大大简化错误处理并增强代码的可维护性。
- thiserror简化实现自定义错误类型,thiserror对于库开发来说是理想的,其中提供的宏对程序员非常友好
- anyhow库集成任何std::error::Error,anyhow适用于内部细节不重要的应用程序,为用户提供简化的信息