跳至主要內容

rust 随笔(三)

Mr.Lexon大约 4 分钟rust

Rust 随笔(三)

thiserror

这篇文章探讨一个常见的错误处理库——thiserror 首先我们回顾一下上一篇的Error第二种用法:

#[derive(Debug)]
pub enum AppError {
    Config(ConfigError),
    Database(DatabaseError),
    Query(QueryError),
    Io(io::Error), // 有时也可能直接暴露一些通用的 IO 错误
    Initialization(String), // 其他初始化错误
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::Config(err) => write!(f, "{}", err), // 直接调用 ConfigError 的 Display
            AppError::Database(err) => write!(f, "{}", err), // 直接调用 DatabaseError 的 Display
            AppError::Query(err) => write!(f, "{}", err),
            AppError::Io(err) => write!(f, "IO 错误: {}", err),
            AppError::Initialization(msg) => write!(f, "初始化错误: {}", msg),
        }
    }
}

impl Error for AppError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            AppError::Config(err) => Some(err),    // ConfigError 实现了 Error,可以作为 source
            AppError::Database(err) => Some(err),  // DatabaseError 实现了 Error
            AppError::Query(err) => Some(err),     // QueryError 实现了 Error
            AppError::Io(err) => Some(err),        // io::Error 本身就实现了 Error
            AppError::Initialization(_) => None,
        }
    }
}

// 实现 From trait
impl From<ConfigError> for AppError {
    fn from(err: ConfigError) -> Self {
        AppError::Config(err)
    }
}

impl From<DatabaseError> for AppError {
    fn from(err: DatabaseError) -> Self {
        AppError::Database(err)
    }
}

impl From<QueryError> for AppError {
    fn from(err: QueryError) -> Self {
        AppError::Query(err)
    }
}

impl From<io::Error> for AppError { // 有时也希望直接转换IO错误
    fn from(err: io::Error) -> Self {
        AppError::Io(err)
    }
}

我们看到,每增加一个错误,就要添加一个impl FromDisplay的枚举分支,那么有什么包可以简化操作呢?我们抬出今天的主角:thiserror。 我们在项目根目录运行:

cargo add thiserror

安装成功之后,我们对上面的代码进行改造:

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

#[derive(Error, Debug)]
pub enum AppError {
	#[error("未知错误")] 
    Unknown,
    
    #[error(transparent)] 
    Config(#[from] ConfigError),

    #[error(transparent)] 
    Database(#[from] DatabaseError),

    #[error(transparent)] 
    Query(#[from] QueryError),

    #[error("IO 错误: {0}")] 
    Io(#[from] io::Error),

    #[error("初始化错误: {0}")]
    Initialization(String),

	#[error("其他错误: {msg},来源:{source}")]
    Other {
	    msg:String,
	    source:String,
    },
}

在这个例子中,有了thiserror的加成,我们极大减少了工作量,现在我们看看这些宏是干什么的:

  1. #[derive(Error, Debug)] 这里的Error是直接指向的是thiserror的宏,他的作用是初始化一些方法和支撑下面属性宏的一些结构
  2. #[error(transparent)]error属性宏有两种用法第一种是这个,这个宏可以直接将枚举包裹内的Error里面的结构以Display的形式提取出来
  3. #[error("初始化错误: {0}")] 第二种则是这个,直接在属性宏中添加错误消息,{0}指的是第一个参数。{n}指的是第n个参数,这个n指的是你在枚举中包裹的参数
  4. #[error("其他错误: {msg},来源:{source}")] 这种和上一个是同一种使用方式,只不过这里直接写成了结构体变体里面的具体的属性名而已。
  5. #[from] 这个指的是从哪个错误类型中转换(就是impl From自动实现) 总的来说thiserror的引入极大减少了在实现错误类型时的工作量。所以这是一个非常有用的依赖。

一个奇怪的东西

你或许已经发现了,在第二个例子中出现了一个奇怪的东西:

#[derive(Error, Debug)]
pub enum AppError {
	//其他的定义...
	#[error("其他错误: {msg},来源:{source}")]
    Other {
	    msg:String,
	    source:String,
    },
}

这是什么东西呢?怎么枚举元素还能这么写?其实,这叫枚举结构体变体struct variant of an enum,它的作用可以在枚举元素里面定义结构体,以包含更多的信息,那怎么使用呢?这里有个例子:

fn main(){
	let app_error = AppError::Other {
	    msg:"测试用".to_string(),
	    source:"测试用".to_string(),
    }
    
	if let AppError::Other { msg, source } = app_error { 
		println!("Message: {}, Source: {}", msg, source); 
	}
}

从以上例子可以看出来,其实这个和结构体的使用几乎没什么区别,配合字段模式简写,几乎在使用时和其他的枚举没什么区别。 那么与之相对的:

  • 单元变体 (Unit-like Variant): 如 Unknown,它不包含任何数据。
  • 元组变体 (Tuple Variant): 如 Initialization(String),它包含一组匿名的、按顺序排列的数据。

总而言之,thiserror 是 Rust 生态中一个强大且便捷的工具。它通过巧妙的宏设计,将我们从手动实现 DisplayError trait 以及繁琐的 From trait 的样板代码中解放出来。我们只需通过声明 #[error(...)] 属性来定义错误信息和来源,即可得到一个功能完善、符合 Rust惯例的错误类型。

希望通过本篇的介绍,你能对 thiserror 有一个清晰的认识,并尝试在你的项目中运用它来改善错误处理的体验!