Rust 枚举还能这样玩?Strum 这个神级库了解一下

0 阅读3分钟

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 生态中最优秀的库几乎都在使用宏写出更简洁、更安全、更易维护的代码。