Rust 枚举还能这样玩?Strum 这个神级库了解一下
很多 Rust 新手都知道可以通过派生宏来扩展枚举的能力,但其实 Rust 社区有一个非常流行的 Enum 增强库——Strum,通过它提供的宏能帮我们解决开发过程中很多的痛点。
当前痛点
在 Rust 中,枚举是一个非常强大的特性,很多语言里的枚举只是几个整数常量:
enum Color { RED, GREEN, BLUE };
而 Rust 的枚举不仅可以携带数据,还能用于状态机、错误处理、协议建模等场景。但还是不够方便,比如:
- 枚举转字符串
- 字符串转枚举
- 遍历所有枚举值
- ...
这些功能如果自己手写,那么就需要手写很多样板代码,说实话挺麻烦的。而这时候就是强大的宏出场的时候了,接下来我们将通过一些使用场景来介绍 Strum 的用法。
首先,我们先在 Cargo.toml 添加上 strum 依赖:
[dependencies]
strum = { version = "0.28", features = ["derive"] }
场景一:枚举转字符串
有这么一个枚举:
enum Status {
Pending,
Running,
Finished,
}
如果需要将枚举转换成字符串,那么一般是通过实现 Display 特征来实现:
use std::fmt;
enum Status {
Pending,
Running,
Finished,
}
impl fmt::Display for Status {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Status::Pending => write!(f, "pending"),
Status::Running => write!(f, "running"),
Status::Finished => write!(f, "finished"),
}
}
}
fn main() {
println!("{}", Status::Pending);
println!("{}", Status::Running);
println!("{}", Status::Finished);
}
而使用 Strum 的 Display 派生宏来实现则非常简单:
use strum::Display;
#[derive(Display)]
enum Status {
#[strum(serialize = "pending")]
Pending,
#[strum(serialize = "running")]
Running,
#[strum(serialize = "finished")]
Finished,
}
fn main() {
println!("{}", Status::Pending);
println!("{}", Status::Running);
println!("{}", Status::Finished);
}
本质上就是 Strum 自动帮我们实现了 Display 特征,说白了就是自动生成这些样板代码。
场景二:字符串转枚举
依旧是这个枚举:
enum Status {
Pending,
Running,
Finished,
}
假设我们现在需要将 pending 字符串转换为对应的枚举,那么可以通过实现 FromStr 特征来实现:
use std::str::FromStr;
#[derive(Debug)]
enum Status {
Pending,
Running,
Finished,
}
impl FromStr for Status {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"pending" => Ok(Status::Pending),
"running" => Ok(Status::Running),
"finished" => Ok(Status::Finished),
_ => Err("invalid".into()),
}
}
}
fn main() {
println!("{:?}", "pending".parse::<Status>());
println!("{:?}", "running".parse::<Status>());
println!("{:?}", "finished".parse::<Status>());
}
这里可以使用 Strum 的 EnumString 派生宏来进行简化:
use strum::EnumString;
#[derive(Debug, EnumString)]
enum Status {
#[strum(serialize = "pending")]
Pending,
#[strum(serialize = "running")]
Running,
#[strum(serialize = "finished")]
Finished,
}
fn main() {
println!("{:?}", "pending".parse::<Status>());
println!("{:?}", "running".parse::<Status>());
println!("{:?}", "finished".parse::<Status>());
}
场景三:遍历所有枚举值
一般情况下,如果我们想遍历枚举值,得专门构建出一个枚举数组来使用,比如:
fn all_status() -> Vec<Status> {
vec![
Status::Running,
Status::Finished,
Status::Cancelled,
]
}
其实这样挺麻烦的,万一有新增或减少枚举值,还得同步进行修改。而使用 Strum 的 EnumIter 派生宏就能解决这个问题:
use strum::{EnumIter, IntoEnumIterator};
#[derive(Debug, EnumIter)]
enum Status {
Pending,
Running,
Finished,
}
fn main() {
for status in Status::iter() {
println!("{:?}", status);
}
}
结语
在这篇文章里,虽然我只讲了三种用法,但是其实已经覆盖了 90% 的 Strum 使用场景了,感兴趣的读者可以自行阅读 Strum 官方文档继续探索。
不过,比起 Strum 本身,我更希望大家关注的是如何善用宏来消除重复代码。
很多 Rust 新手在学习时会下意识地避开宏,确实它很难,但你会发现宏不单是 Rust 的高级技巧,还是写出地道 Rust 代码的关键手段之一。Rust 生态中最优秀的库几乎都在使用宏写出更简洁、更安全、更易维护的代码。