rust 随笔(三)
大约 4 分钟
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 From和Display的枚举分支,那么有什么包可以简化操作呢?我们抬出今天的主角: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的加成,我们极大减少了工作量,现在我们看看这些宏是干什么的:
#[derive(Error, Debug)]这里的Error是直接指向的是thiserror的宏,他的作用是初始化一些方法和支撑下面属性宏的一些结构#[error(transparent)]error属性宏有两种用法第一种是这个,这个宏可以直接将枚举包裹内的Error里面的结构以Display的形式提取出来#[error("初始化错误: {0}")]第二种则是这个,直接在属性宏中添加错误消息,{0}指的是第一个参数。{n}指的是第n个参数,这个n指的是你在枚举中包裹的参数#[error("其他错误: {msg},来源:{source}")]这种和上一个是同一种使用方式,只不过这里直接写成了结构体变体里面的具体的属性名而已。#[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 生态中一个强大且便捷的工具。它通过巧妙的宏设计,将我们从手动实现 Display、Error trait 以及繁琐的 From trait 的样板代码中解放出来。我们只需通过声明 #[error(...)] 属性来定义错误信息和来源,即可得到一个功能完善、符合 Rust惯例的错误类型。
希望通过本篇的介绍,你能对 thiserror 有一个清晰的认识,并尝试在你的项目中运用它来改善错误处理的体验!