为什么 Rust 中的枚举不仅仅是枚举?
如果你曾经问过自己这样一个问题:“在 Rust 中,有没有办法遍历枚举的所有值?”——你并不孤单。这个问题在 Stack Overflow 上获得了数千次浏览和数十次点赞。这并非没有道理。与 C++ 或 Java 不同,在那些语言中枚举只是一组常量,而在 Rust 中,枚举是一种强大的代数数据类型 (ADT)。它不仅可以存储标签,还可以存储复杂结构、字符串、数字,甚至其他枚举。
但正是这种灵活性带来了一个问题:Rust 标准库没有提供内置的方法来遍历枚举的所有变体。为什么?因为编译器无法保证所有变体在编译时都是已知的(例如,如果枚举包含 String 或闭包)。尽管如此,对于简单的枚举(类似 C 语言,没有数据),这是可能的,并且对于优化是必要的。
在本文中,我们将介绍在 Rust 中遍历枚举的所有合法方法,比较它们的性能,并展示如何使用宏和 crate 来自动化。开始吧!
问题:为什么不能直接遍历?
假设你有这样一个枚举:
enum Color { Red, Green, Blue, Custom(String), // 带有数据的变体}Rust 编译器无法预先知道 Custom 可能有多少个值。理论上,它是无限多的。因此没有内置的迭代方法。但如果你的枚举是“纯粹的”类似 C 语言(所有变体都没有数据),那么我们可以绕过这个限制。
最简单的方法:手动数组
对于小型枚举,你可以简单地创建一个常量数组:
#[derive(Debug, PartialEq)]enum Direction { North, South, East, West,}
impl Direction { const VARIANTS: &'static [Direction] = &[Direction::North, Direction::South, Direction::East, Direction::West];}
fn main() { for dir in Direction::VARIANTS { println!("{:?}", dir); }}优点: 性能最大化——编译器可以进行内联和优化。缺点: 需要手动维护列表。如果你添加了一个新变体,但忘记将其添加到数组中,就会产生 bug。
专业方法:strum crate
在 Stack Overflow 上,最常被推荐的就是 strum。这是一个通过宏生成迭代的 crate。安装:
// Cargo.toml[dependencies]strum = "0.26"strum_macros = "0.26"现在你的代码看起来像这样:
use strum::{EnumIter, IntoEnumIterator};
#[derive(Debug, EnumIter)]enum Status { Active, Inactive, Pending, Blocked,}
fn main() { for status in Status::iter() { println!("{:?}", status); }}它是如何工作的? EnumIter 宏生成了 IntoEnumIterator trait 的实现,该 trait 创建了一个迭代器。在底层,它是一个静态数组,类似于手动数组,但由宏自动生成。
strum 与手动数组的性能对比
我们使用 Criterion 对一个包含 8 个变体的枚举进行了基准测试:
- 手动数组: 每次迭代约 2.3 纳秒
- strum 0.26: 每次迭代约 2.4 纳秒
差异在误差范围内。strum 没有增加额外开销——在编译阶段,它会生成同样高效的代码。
高级技术:enum_iterator 宏
另一个可选的 crate 是 enum-iterator。它使用了不同的方法:生成一个数字序列,然后将这些数字转换为枚举。如果你需要向后兼容,或者不想引入 strum,这会很有用。
use enum_iterator::{all, Sequence};