no script

你的浏览器禁用了JavaScript, 请开启后刷新浏览器获得更好的体验!

进入课程

Rust 错误处理的“左右手”:anyhow 和 thiserror

​ 今天咱们来唠唠 Rust 开发中两个超实用的错误处理库——anyhowthiserror。要是你已经在 Rust 的世界里摸爬滚打了一段时间,那这两个名字肯定不陌生。它们就像是 Rust 错误处理的“左右手”,各有各的厉害,各有各的用处。接下来,我就带大家好好了解一下它们,顺便比比谁更适合你手头的项目。

1. 认识 anyhow:简单粗暴的“救火队长”

先说说anyhow吧。这哥们儿简直就是个“救火队长”,遇到错误,不管三七二十一,先给你扑灭再说。它的核心思想特别简单:用一个通用的错误类型anyhow::Error,把各种各样的错误都装进去。这样一来,你再也不用操心那些复杂的错误类型定义了,直接甩一个anyhow!宏,问题搞定!

anyhow具备以下几个关键特性:

  • 良好的易用性anyhow 提供了一个通用的错误类型anyhow::Error,这个类型可以包含任何实现了 std::error::Error 的错误。这意味着你可以使用 anyhow::Error 来包装几乎所有类型的错误,无需担心具体的错误类型。
  • 简洁的错误传播: anyhow 支持通过 ? 操作符来传播错误,同时保留错误发生的上下文。这让错误处理更加直观,同时还能保留错误链,便于调试。
  • 方便的开发调试: anyhow 支持通过 {:#} 格式化指示符来打印错误及其所有相关的上下文和原因,这使得调试复杂的错误链变得更加简单。
  • 透明的错误类型: 在开发项目过程中,你只需要将错误信息传递给上一层函数,而不需要关心错误的具体类型。anyhow 让这一过程变得简单,因为它可以包装任何错误,而不需要显式地指定错误类型。

使用 anyhow 的典型场景包括快速原型开发、应用程序顶层的错误处理,或者在库中作为返回错误类型的一个简便选择,尤其是你开发的crate被第三方使用时,第三方可以不需要关心具体的错误类型。一般来说,anyhow可通过如下几种方式提供错误处理:

  • 使用 Result<T, anyhow::Error> 或者 anyhow::Result<T> 作为返回值,然后利用 ? 语法糖无脑传播报错。
  • 使用 with_context(f)或者 context(t) 来附加错误信息。
  • 使用 map_err 将标准库的错误转换为 anyhow:Error
  • 使用 anyhow:bail宏提前返回错误,相当于return Err(anyhow!($args...))
  • 使用 downcast 反解具体的错误类型。

在开发调试过程中,可以使用如下几种方式打印anyhow

  1. 可以使用 {} 或者 .to_string(),但是仅仅打印最外层错误或者上下文,而不是内层的错误;
  2. 可以使用 {:#} 打印外层和底层错误;
  3. 可以使用 {:?} 在调试模式打印错误以及调用栈;
  4. 可以使用 {:#?} 以结构体样式打印错误;
Error {
  context: "Failed to read contents from ./path/to/anyhow.txt",
  source: Os {
      code: 2,
      kind: NotFound,
      message: "No such file or directory",
  },
}
1.1 使用 anyhow!宏

anyhow可太简单了,就像吃个汉堡一样轻松。首先,得在你的Cargo.toml文件里加上它:

[dependencies]
anyhow = "1.0"

然后,你就可以在代码里尽情发挥啦。比如,直接用anyhow!抛个错误:

use anyhow::{anyhow, Error, Result};
fn use_anyhow() -> Result<()> {
    Err(anyhow!("this is error"))
}

使用 anyhow! 这个宏可以生成 anyhow::Error类型的值,它可以接受字符串,格式化字符串作为参数,或者实现 std::error:Error 的错误作为参数。

1.2 使用 ?操作符

anyhow 的一个重要特性是可以与 ? 操作符一起使用,自动将不同的错误类型转换为 anyhow::Error

use std::fs::File;
use anyhow::Result;
fn open_file(path: &str) -> Result<File> {
	let file = File::open(path)?;
    ok(file)
}

如果 File::open 返回一个错误,? 操作符会自动将其转换为 anyhow::Error,并返回。

1.3 添加上下文信息

有时候,一个简单的错误消息还不够,你可能想加点上下文,让别人更清楚地知道问题出在哪里。anyhow给你准备了contextwith_context方法,这就好比在伤口上贴个标签,写上受伤的原因。比如:

use std::fs::File;
use anyhow::{Context, Result};

fn open_file(path: &str) -> Result<File> {
    let file = File::open(path).context("Failed to open file")?;
    ok(file)
}
fn open_file(path: &str) -> Result<File> {
    let file = File::open(path).context(format!("Failed to open file: {}", path))?;
    ok(file)
}
fn open_file(path: &str) -> Result<File> {
    let file = File::open(path).with_context(|| format!("Failed to open file: {}", path))?;
    ok(file)
}

这样,当错误发生的时候,你不仅能知道是文件读取失败了,还能知道是哪个文件出了问题。

1.4 使用错误链

anyhow::Error 支持错误链,即一个错误可以包含另一个错误的信息。这对于调试复杂问题非常有用:

use std::fs::File;
use anyhow::{anyhow, Result};
fn open_file(path: &str) -> Result<File> {
    let file = File::open(path).map_err(|e| anyhow!("Failed to open file: {}", e))?;
    ok(file)
}

map_err 被用来将标准库的错误转换为 anyhow::Error,并添加额外的上下文信息。

1.5 使用anyhow::bail宏

anyhow::bail宏用于提前错误返回,等价于 return Err(anyhow!($args...))

use anyhow::{bail, Result};
fn open_file(path: &str) -> Result<File> {
    let result = File::open(path);
    if result.is_err() {
        bail!("Failed to open file: {}", result.unwrap_err());
    }
    ok(result)
}

unwrap_err()函数用于提取result枚举变体Err的错误信息。

1.6 使用downcast反解错误类型

anyhow 提供了 downcast_ref方法,用于在运行时将 anyhow::Error 转换为其包含的具体错误类型的引用。这可以用于检查和处理特定类型的错误。

use anyhow::{Result, Context};
use std::{fs, io};

fn read_and_process_file(file_path: &str) -> Result<()> {
    // 尝试读取文件
    let data = fs::read_to_string(file_path)
        .with_context(||format!("failed to read file `{}`", file_path))?;
    // 解析数据
    let processed_data = parse_data(&data)
        .with_context(||format!("failed to parse data from file `{}`", file_path))?;
    // 执行数据操作
    perform_some_operation(processed_data)
        .with_context(|| "failed to perform operation based on file data")?;
    Ok(())
}

fn parse_data(data: &str) -> Result<String> {
    Ok(data.to_uppercase())
}

fn perform_some_operation(data: String) -> Result<()> {
    println!("processed data: {}", data);
    Ok(())
}

fn main() {
    let file_path = "./anyhow.txt";
    let res =  read_and_process_file(file_path);
    match res {
        Ok(_) => println!("successfully!"),
        Err(e) => {
            // 使用 downcast 来反解出实际的错误类型,可能出现的异常只能是 io::Error
            if let Some(my_error) = e.downcast_ref::<io::Error>() {
                println!("has io error: {:#}", my_error);
            } else {
                println!("unknown error: {:?}", e);
            }
        }
    }
}

2. 认识 thiserror:精雕细琢的“工匠”

说完anyhow,咱们再来看看thiserror。这哥们儿更像是个“工匠”,注重细节,追求完美。它让你自己定义错误类型,每个错误都有自己的名字和描述。虽然用起来比anyhow复杂一点,但好处是,你能更清楚地知道每个错误到底是什么意思。

2.1 怎么用 thiserror

thiserror,首先得在Cargo.toml里加上它:

[dependencies]
thiserror = "2.0"

2.0以上版本要求编译器 rustc 1.61+, 然后,你就可以开始定义自己的错误类型了:

use std::io;
use thiserror::Error;

#[derive(Error,Debug)]
pub enum DataStoreError {
    #[error("data store disconnected")]
    Disconnect(#[from] std::io::Error),
    #[error("the data for key `{0}` is not available")]
    Redaction(String),
    #[error("invalid header (expected {expected:?},found {found:?})")]
    InvalidHeader {expected:String,found:String},
    #[error("unknown data store error")]
    Unknown
}

fn main() {
    println!("Unknown {}", DataStoreError::Unknown);
    println!("InvalidHeader {}", DataStoreError::InvalidHeader {
        expected : String::from("expected"),
        found : String::from("found")
    });
    println!("Redaction {}",DataStoreError::Redaction(String::from("Redaction")));
    println!("Disconnect {}",DataStoreError::Disconnect(io::Error::from(io::ErrorKind::TimedOut)));
}

看,每个错误都有自己的名字和描述,清清楚楚。要是你在函数里遇到错误,直接用?thiserror会帮你把错误类型转换好,就像一个贴心的助手。

2.2 使用#[error]

如果使用 #[error(...)] 为枚举或者结构体生成自定义错误消息,这将为它们实现 Display

  • #[error("{var}")] ⟶ write!("{}", self.var)
  • #[error("{0}")] ⟶ write!("{}", self.0)
  • #[error("{var:?}")] ⟶ write!("{:?}", self.var)
  • #[error("{0:?}")] ⟶ write!("{:?}", self.0)
2.2.1 枚举
use thiserror::Error;

#[derive(Debug)]
pub struct Limits{
    lo : i16,
    hi : i16
}

#[derive(Error,Debug)]
pub enum MyError{
    #[error("invalid rdo_lookahead_frames {0} (expected < {max})", max = i32::MAX)]
    InvalidLookahead(u32),
    #[error("first letter must be lowercase but was {:?}", first_char(.0).unwrap_or(char::default()))]
    WrongCase(String),
    #[error("invalid index {idx},expected at least {} and at most {}", .limits.lo, .limits.hi)]
    OutOfBounds{idx:usize, limits:Limits}
}

fn first_char(s: &str) -> Option<char> {
    let first = s.chars().next()?;
	Some(first)
}

fn main() {
    println!("InvalidLookahead {}",MyError::InvalidLookahead(3333));
    println!("WrongCase {}",MyError::WrongCase("kk".to_string()));
    println!("OutOfBounds {}",MyError::OutOfBounds{idx : 89,limits:Limits{
        lo:12,
        hi:11
    }});
}
2.2.2 结构体
use thiserror::Error;

#[derive(Error, Debug)]
#[error("username {username}(money:{money})")]
struct MyErrorStruct {
    username: String,
    money: u32,
}

fn check_money(money: u32) -> Result<(), MyErrorStruct> {
    if money < 100 {
        Err(MyErrorStruct {
            username: "jack.ma".into(),
            money,
        })
    } else {
        Ok(())
    }
}

fn main() {
	if let Err(e) = check_money(99) {
		println!("check money: {}", e);
    }
}
2.3 使用#[from]

错误类型转换:#[error(transparent)]属性意味着该错误只是作为其他错误的容器,它的错误消息将直接从其“源”错误中继承。
#[from]属性标记意味着io::Error可以自动转换为MyError::IoError

use std::io;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum MyError {
	#[error(transparent)]
	IOError(#[from] io::Error),
}

fn main() {
	println!("MyError IOError: {}", MyError::IOError(io::Error::from(io::ErrorKind::TimedOut)));
	if let Err(e) = std::fs::File::open("thiserror.txt") {
		println!("open file: {}", e);
	}
}

嵌套错误:错误链允许捕获并响应从底层库或函数传播出来的错误。

#[derive(Error, Debug)]
pub enum MyError {
	#[error("io error occurred")]
	IOError(#[from] io::Error),
}

fn main() {
	if let Err(e) = std::fs::read_to_string("thiserror.txt").map_err(MyError::from) {
		println!("{:#?}", e);
	}
}
2.4 使用#[source]

使字段命名为 source,可为自定义错误实现 source 方法,返回底层的错误类型:

use std::io;
use thiserror::Error;
use std::error::Error;

#[derive(Error, Debug)]
pub enum MyError {
  #[error("some io error happened, {:?}", .source)]
  IOError { source: io::Error },
}

fn main() {
  let err = MyError::IOError {
      source: io::Error::from(io::ErrorKind::TimedOut),
  };
  println!("{:?}", err.source());
}

使用 #[source] 属性标记非 source 的字段:

#[derive(Error, Debug)]
pub enum MyError {
  #[error("some io error happened, {:?}", .err)]
  IO {
      #[source]
      err: io::Error,
  },
}

fn main() {
  let err = MyError::IO {
      err: io::Error::from(io::ErrorKind::TimedOut),
  };
  println!("{:?}", err.source());
}

#[from]#[source] 二选一即可,#[from] 也会为类型生成 .source() 方法:

use std::error::Error as _;
use std::io;
use thiserror::Error;

#[derive(Error, Debug)]
#[error("some io error happened, {:?}", .source)]
pub struct MyError {
  #[from]
  source: io::Error,
}

fn main() {
  let err = MyError::from(io::Error::from(io::ErrorKind::TimedOut));
  println!("{:?}", err.source());
}

3. anyhow 和 thiserror 的“擂台赛”

好啦,现在咱们已经认识了anyhowthiserror,接下来咱们来比比它们,看看谁更适合你。

3.1 错误类型:简单 vs 精细

anyhow是个“大杂烩”,不管什么错误,统统装进anyhow::Error里。好处是简单,坏处是不够精细。thiserror则是个“细节控”,让你自己定义错误类型,每个错误都有自己的名字和描述。好处是清晰,坏处是稍微复杂一点。

3.2 上下文信息:自动 vs 手动

anyhow的上下文信息是自动记录的,你啥都不用管,它自己就帮你搞定。thiserror则需要你手动定义上下文信息,虽然麻烦一点,但能让你更清楚地表达错误的含义。

3.3 使用场景:快速开发 vs 精准控制

如果你只是想快速开发一个功能,不想被复杂的错误类型定义困扰,anyhow绝对是你的不二之选。它就像一把瑞士军刀,虽然功能简单,但足够应对大部分情况。如果你的项目对错误处理要求特别高,需要精确控制每个错误的类型和上下文,那thiserror就是你的“秘密武器”。

3.4 anyhow和thiserror配合使用

通常thiserror用于对外封装的库crate实现,提供对外的精确错误控制类型,而anyhow则更方便的用于内部逻辑实现。

use thiserror::Error;
use std::fs::{self, File};
use anyhow::{Context, Result};

fn open_file(path: &str) -> Result<File> {
    let file = File::open(path).context(format!("Failed to open file: {}", path))?;
    Ok(file)
}

fn read_file(path: &str) -> Result<String> {
    let data = fs::read_to_string(path).context(format!("Failed to read file: {}", path))?;
    Ok(data)
}

#[derive(Error, Debug)]
pub enum AnyOpenError {
	#[error(transparent)]
	OpenFileError(#[from] anyhow::Error),
}

#[derive(Error, Debug)]
pub enum AnyReadError {
	#[error(transparent)]
	ReadFileError(#[from] anyhow::Error),
}


#[derive(Error, Debug)]
pub enum MyError {
	#[error(transparent)]
	OpenError(AnyOpenError),
	#[error(transparent)]
	ReadError(AnyReadError),
}

fn process(path: &str) -> core::result::Result<(), MyError> {
	
	if let Err(e) = open_file(path) {
		return Err(MyError::OpenError(AnyOpenError::from(e)))
	}
	
	if let Err(e) = read_file(path) {
		return Err(MyError::ReadError(AnyReadError::from(e)))
	}
	
	Ok(())
}

fn main() {
	if let Err(e) = process("thiserror.txt") {
		println!("{:#?}", e);
	}
}

4.写在最后

好了,朋友们,今天就聊到这里。anyhowthiserror都是 Rust 错误处理的利器,各有各的用处。anyhow像个“救火队长”,简单直接,适合快速开发;thiserror像个“工匠”,注重细节,适合需要精准控制的场景。希望这篇文章能帮你在开发中更好地选择合适的工具,让代码更优雅,让生活更美好!

0
chujiao_0a9e6**7125
盘丝大仙我的剑只有我的心上人才能拔出
  • 赞同
  • 威望

相关问题

    Copyright © 2025