Как в Rust перебрать все варианты перечисления (enum)?
Решение: используем крейт strum для итерации по вариантам enum
В Rust нет встроенного способа итерации по вариантам перечисления, потому что язык не хранит метаинформацию о количестве и порядке вариантов во время выполнения. Однако это легко решается с помощью крейта strum, который генерирует нужный код через derive-макросы.
Шаг 1: Добавьте зависимость
В Cargo.toml добавьте:
[dependencies]
strum = "0.26"
strum_macros = "0.26"
Шаг 2: Используйте макрос EnumIter
Добавьте атрибут #[derive(EnumIter)] к вашему перечислению:
use strum::EnumIter;
use strum::IntoEnumIterator;
#[derive(Debug, EnumIter)]
enum Color {
Red,
Green,
Blue,
Yellow,
}
fn main() {
for color in Color::iter() {
println!("{:?}", color);
}
}
Как это работает?
#[derive(EnumIter)]генерирует реализацию трейтаIntoEnumIteratorдля вашего enum.- Метод
Color::iter()возвращает итератор по всем вариантам в порядке их объявления. - Итератор ленивый — варианты создаются только при обращении к ним.
Продвинутые возможности
Крейт strum предоставляет и другие полезные макросы:
use strum::{EnumCount, EnumString, Display};
#[derive(Debug, EnumIter, EnumCount, EnumString, Display)]
enum Color {
Red,
Green,
Blue,
Yellow,
}
fn main() {
// Количество вариантов
println!("Всего цветов: {}", Color::COUNT); // 4
// Преобразование строки в вариант
let color: Color = "Red".parse().unwrap();
// Отображение имени варианта
println!("{}", color); // "Red"
// Собрать все варианты в вектор
let colors: Vec<Color> = Color::iter().collect();
println!("{:?}", colors);
}
Что если enum содержит данные?
Если варианты имеют связанные данные, нужно указать значения по умолчанию:
use strum::EnumIter;
#[derive(Debug, EnumIter)]
enum HttpStatus {
Ok = 200,
NotFound = 404,
ServerError = 500,
}
// Для вариантов с данными используйте Default
#[derive(Debug, EnumIter, Default)]
enum Message {
#[default]
Text(String), // потребуется значение по умолчанию
Quit,
}
Важно: Для вариантов с данными (например, Text(String)) нужно либо реализовать Default, либо использовать атрибут #[default], чтобы strum знал, какое значение использовать при итерации.
Альтернатива без внешних зависимостей
Если вы не хотите подключать крейты, можно написать простую функцию вручную:
#[derive(Debug, Clone, Copy)]
enum Color {
Red,
Green,
Blue,
Yellow,
}
impl Color {
fn all() -> [Color; 4] {
[Color::Red, Color::Green, Color::Blue, Color::Yellow]
}
}
fn main() {
for color in Color::all().iter() {
println!("{:?}", color);
}
}
Но такой подход требует ручного обновления массива при добавлении новых вариантов, что чревато ошибками. Поэтому strum — рекомендуемое решение для production-кода.
Использование match и срезов для небольших enum
Если ваш enum маленький и стабильный (редко меняется), можно обойтись без внешних зависимостей, используя match и коллекцию:
#[derive(Debug, Clone, Copy)]
enum Color {
Red,
Green,
Blue,
Yellow,
}
impl Color {
fn all() -> Vec<Color> {
vec![
Color::Red,
Color::Green,
Color::Blue,
Color::Yellow,
]
}
}
fn main() {
for color in Color::all() {
println!("{:?}", color);
}
}
Макрос для автоматизации
Можно написать свой макрос, чтобы не дублировать варианты:
macro_rules! enum_iter {
($name:ident { $($variant:ident),* }) => {
impl $name {
fn all() -> Vec<$name> {
vec![$($name::$variant),*]
}
}
};
}
enum_iter!(Color { Red, Green, Blue, Yellow });
fn main() {
for color in Color::all() {
println!("{:?}", color);
}
}
Этот подход полезен, когда вы хотите избежать зависимости от strum в маленьких проектах или когда enum гарантированно не будет расширяться. Однако для больших проектов с часто меняющимися enum всё же рекомендую strum — он автоматически подстраивается под изменения и предоставляет дополнительные удобства.
Примечание: Если ваш enum содержит варианты с данными (например, Point { x: i32, y: i32 }), ручной подход становится проблематичным — нужно придумывать значения по умолчанию для каждого поля. В таких случаях strum снова выручает.