在Rust中,是否有一种方法可以遍历枚举类型的值?

161

我来自Java的背景,可能会像这样定义一个枚举类型enum Direction { NORTH, SOUTH, EAST, WEST},然后使用增强for循环对每个值进行操作:

for(Direction dir : Direction.values())  {
    //do something with dir
}
我希望能够使用Rust枚举来做类似的事情。

有一个关于 Enum 特性的问题(https://github.com/mozilla/rust/issues/5417),可能可以派生出类似 for dir in Direction::values() { ... } 的东西(也许)。 - huon
11个回答

158
您可以使用 strum crate 来轻松遍历枚举值。
use strum::IntoEnumIterator; // 0.17.1
use strum_macros::EnumIter; // 0.17.1

#[derive(Debug, EnumIter)]
enum Direction {
    NORTH,
    SOUTH,
    EAST,
    WEST,
}

fn main() {
    for direction in Direction::iter() {
        println!("{:?}", direction);
    }
}

输出:

NORTH
SOUTH
EAST
WEST

2
还有一个名为 enum-iterator crate 的工具,通过 Sequence 特性和 all 函数提供类似的功能,支持嵌套枚举类型。这是它的优势所在。 - Tad Lispy
@abe死亡游乐场链接 - jeyko
请注意,这似乎在当前(截至本文撰写时)稳定的 Rust 1.67.1 上失败 - playground - abe
@jeyko 谢谢!已更新带永久链接 -- 如果还有问题请告诉我。 - abe
1
@abe 这是一个 Playground 的问题。它只支持前100个 crate。你可以在Rust Explorer上看到 strum 的运行情况。 - John Kugelman

65

如果枚举是类C的(就像您的示例一样),那么您可以创建每个变量的静态数组,并返回对它们引用的迭代器:

如果枚举类似于C语言中的枚举类型(如您提供的示例),则可以创建一个包含每个枚举变量的静态数组,并返回该数组元素的引用迭代器:

use self::Direction::*;
use std::slice::Iter;

#[derive(Debug)]
pub enum Direction {
    North,
    South,
    East,
    West,
}

impl Direction {
    pub fn iterator() -> Iter<'static, Direction> {
        static DIRECTIONS: [Direction; 4] = [North, South, East, West];
        DIRECTIONS.iter()
    }
}

fn main() {
    for dir in Direction::iterator() {
        println!("{:?}", dir);
    }
}

如果您使枚举实现Copy,则可以使用Iterator :: copied 并返回 impl Trait 以获得值的迭代器:

impl Direction {
    pub fn iterator() -> impl Iterator<Item = Direction> {
        [North, South, East, West].iter().copied()
    }
}

另请参阅:


这似乎比在 Rust 中使用“枚举”更好。我真的想知道 Rust 是否应该将它们的枚举称为联合或其他什么名称。此外,作为强大的变体,您可以关联其他信息,例如:[North(0), South(180), EAST(90), WEST(270)]等,或者更多,给定元组的帮助... - bluejekyll
5
这种方法的问题在于,你需要使数组与枚举常量同步。如果你添加了一个常量但忘记调整 iterator() 方法,那么就会出现错误。 strum(或类似的库)可以解决这个问题。 - TheOperator
1
@TheOperator的评论很公正,尽管我要补充一点,出于各种原因(如性能或使用高阶函数/闭包时需要灵活性),有时我会保留对enum的静态映射,所以我个人认为这仍然是一个不错的方法。如果有人想知道use self::Direction::*;是什么意思,那是为了将enum值带入命名空间,因此如果您愿意,可以删除它并将[North...替换为[Direction :: North... - abe

57
没有,我认为这是因为Rust中的枚举比Java中的强大得多 - 实际上它们是完整的代数数据类型。例如,您希望如何迭代此枚举的值:
enum Option<T> {
    None,
    Some(T)
}

?

它的第二个成员Some不是静态常量,你可以使用它来创建Option<T>类型的值:

let x = Some(1);
let y = Some("abc");

所以,没有任何明智的方法可以迭代任何枚举的值。
当然,我认为,可能会将对静态枚举(即仅具有静态项的枚举)的特殊支持添加到编译器中,这样它就会生成一些函数来返回枚举的值或包含它们的静态向量,但我认为编译器中的额外复杂性并不值得。
如果您真的需要此功能,则可以编写自定义语法扩展(请参见this问题)。该扩展应接收标识符列表并输出一个枚举和一个包含这些标识符作为内容的静态常量向量。常规宏也可以在某种程度上起作用,但据我所知,您无法两次转录宏参数的重复性,因此您必须手动两次编写枚举元素,这不方便。
此外,这个问题可能会引起一些兴趣:#5417 当然,您始终可以手动编写返回枚举元素列表的代码。

这很有帮助。我认为对我来说最容易的事情可能是编写一个返回值列表或迭代器等的枚举实现。谢谢! - dougli1sqrd
84
我尊重地不同意枚举类型不值得使用的说法。它们被称为枚举类型,正是因为这是一种常见而有用的方法!当然,拥有代数能力非常重要,但对于最常见的情况来说,方便性也很重要。 - Rex Kerr
1
@RexKerr,这只是我对为什么这个功能还没有出现的原因的建议。我相信停止了相应问题的讨论支持我的观点。顺便说一句,当我需要一次处理所有枚举元素时,通常是某种索引(例如哈希映射),它允许通过某个键检索枚举值,并且这个索引无论如何都必须手动构建。 - Vladimir Matveev
3
@VladimirMatveev - 嗯,应该尽量少手动构建可以自动构建的东西。这不正是有编译器的意义吗?但我同意你的分析。 - Rex Kerr
这将是值得的。并且一个已实例化的枚举Option<u32>可以很好地进行迭代。 - stimulate

11

我在crate plain_enum中实现了基本功能。

它可以用于声明类似于C语言的枚举,如下所示:

#[macro_use]
extern crate plain_enum;

plain_enum_mod!(module_for_enum, EnumName {
    EnumVal1,
    EnumVal2,
    EnumVal3,
});

然后将允许您执行以下操作:

for value in EnumName::values() {
    // do things with value
}

let enummap = EnumName::map_from_fn(|value| {
    convert_enum_value_to_mapped_value(value)
})

9
你可以使用关联常量:
impl Direction {
    const VALUES: [Self; 4] = [Self::NORTH, Self::SOUTH, Self::EAST, Self::WEST];
}

fn main() {
    for direction in Direction::VALUES.iter().copied() {
        todo!();
    }
}

2

3
你的回答可以通过提供更多支持信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人可以确认你的答案是正确的。您可以在帮助中心找到有关如何编写良好答案的更多信息。 - Community

2

如果您不想导入第三方的crate,您可以编写自己的宏来实现。以下是我如何实现的(可能有改进的方法):

macro_rules! iterable_enum {
    ($visibility:vis, $name:ident, $($member:tt),*) => {
        $visibility enum $name {$($member),*}
        impl $name {
            fn iterate() -> Vec<$name> {
                vec![$($name::$member,)*]
            }
        }
    };
    ($name:ident, $($member:tt),*) => {
        iterable_enum!(, $name, $($member),*)
    };
}

然后您可以执行以下操作:

iterable_enum!(pub, EnumName, Value1, Value2, Value3);

fn main() {
    for member in EnumName::iterate() {
        // ...
    }
}

该实现仅限于简单的枚举类型。考虑以下枚举类型,您如何迭代它?:

enum MoreComplexEnum<T1, T2> {
    One(T1),
    Two(T2),
    Other,
    Both(T1, T2),
    Error(String),
}

由于 Rust 中的枚举类型非常强大,因此实现一个完全可迭代的枚举类型可能会比较困难,因为它们与 C 或 Java 中的简单枚举类型不同。

向量的堆分配确实不是必需的。 - koral

1
如果枚举元素具有相同的大小,您可以使用这个。
#[repr(usize)]
enum SomeEnum {
   X = 0,
   Y = 1,
   Z = 2,
}

pub fn iterate_enum(start: usize, end: usize) -> impl Iterator<Item=SomeEnum> {
    unsafe {(start..end).map(|num| transmute(num))}
}

0

这是我对@Ahmed Merez的答案的看法:

  • 不在堆上分配
  • 是const
  • 接受(几乎)实际枚举作为输入(需要vis,因为Rust似乎不喜欢带有错误的空可见性参数error: repetition matches empty token tree), 包括:
    • 派生输入
    • 属性属性
#[macro_export]
macro_rules! count {
    () => (0usize);
    ( $x:tt $($xs:tt)* ) => (1usize + $crate::count!($($xs)*));
}

/// https://dev59.com/4GEi5IYBdhLWcg3wWLES#64678145
macro_rules! iterable_enum {
    ($(#[$derives:meta])* $(vis $visibility:vis)? enum $name:ident { $($(#[$nested_meta:meta])* $member:ident),* }) => {
        const count_members:usize = $crate::count!($($member)*);
        $(#[$derives])*
        $($visibility)? enum $name {
            $($(#[$nested_meta])* $member),*
        }
        impl $name {
            pub const fn iter() -> [$name; count_members] {
                [$($name::$member,)*]
            }
        }
    };
}


fn main() {
    iterable_enum! {
        #[derive(Debug, serde::Deserialize)]
        vis pub(crate) enum X {
            #[serde(rename="a")]
            A,
            B
        }
    }
    
    for x in X::iter() {
        dbg!(x);
    }
}

0

我对 @koral 答案的改进

  • 不需要 vis
  • 实现了 into_iter,返回真正的迭代器而非数组
macro_rules! iterable_enum {(
  $(#[$derives:meta])*
  $pub:vis enum $name:ident {
    $(
      $(#[$nested_meta:meta])*
      $member:ident,
    )*
  }) => {
    const _MEMBERS_COUNT:usize = iterable_enum!(@count $($member)*);
    $(#[$derives])*
    $pub enum $name {
      $($(#[$nested_meta])* $member),*
    }
    impl $name {
      pub fn into_iter() -> std::array::IntoIter<$name, _MEMBERS_COUNT> {
        [$($name::$member,)*].into_iter()
      }
    }
  };
  (@count) => (0usize);
  (@count  $x:tt $($xs:tt)* ) => (1usize + iterable_enum!(@count $($xs)*));
}

fn main() {
  iterable_enum! {
  #[derive(Debug, serde::Deserialize)]
  pub enum X {
    #[serde(rename="a")]
    A,
    B,
  }}
    
  X::into_iter().fold(...);
}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接