Avatar ·

Как в Rust перебрать все варианты перечисления (enum)?

Всем привет! Я только начал изучать Rust и столкнулся с проблемой. У меня есть перечисление, которое описывает цвета: ```rust enum Color { Red, Green, Blue, Yellow, } ``` Мне нужно вывести все возможные цвета на экран или, например, сохранить их в вектор. В других языках, вроде Java или C#, я бы использовал `values()` или что-то подобное. Но в Rust я не нашел встроенного способа просто перебрать все варианты `enum`. Я пробовал написать что-то вроде: ```rust for color in Color { println!("{:?}", color); } ``` Но компилятор ругается, что `Color` не реализует трейт `Iterator`. Понятно, что Rust — язык системный и у него своя философия, но неужели нет простого способа перебрать все константы перечисления? Может быть, есть какой-нибудь макрос или библиотека? Или нужно каждый раз вручную писать функцию, которая возвращает все варианты? Подскажите, пожалуйста, как это делается правильно. Заранее спасибо! <<>>

Решение: используем крейт 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 снова выручает.