Avatar ·

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

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

Прямой итерации нет — используйте вспомогательные структуры

В Rust действительно нет встроенного способа итерации по вариантам перечисления, потому что перечисления — это не коллекции, а типы-суммы. Каждый вариант может содержать разные данные, и компилятор не может гарантировать, что все варианты "пусты" (как в вашем примере). Однако есть несколько проверенных подходов:

1. Использование макроса strum (рекомендуемый способ)

Самый популярный и удобный метод — подключить крейт strum и его макрос EnumIter. Это добавляет трейт IntoEnumIterator к вашему перечислению.

Добавьте в Cargo.toml:

[dependencies]
strum = "0.26"
strum_macros = "0.26"

Теперь ваш код будет выглядеть так:

use strum::IntoEnumIterator;
use strum_macros::EnumIter;

#[derive(Debug, EnumIter)]
enum Color {
    Red,
    Green,
    Blue,
    Yellow,
}

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

Макрос автоматически генерирует итератор, который перебирает все варианты в порядке их объявления. Это работает только для перечислений без данных (unit-вариантов). Если у вас варианты с данными, их нужно будет обрабатывать отдельно.

2. Ручная реализация через массив констант

Если вы не хотите подключать внешние зависимости, можно создать массив всех вариантов вручную. Это просто и не требует макросов:

#[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);
    }
}

Недостаток: если вы добавите новый вариант, придётся обновить массив вручную. Зато нет зависимостей, и код полностью прозрачен.

3. Использование match для фиксированного набора

Если нужно просто выполнить действие для каждого варианта, можно обойтись без итерации:

fn process_colors() {
    let colors = [Color::Red, Color::Green, Color::Blue, Color::Yellow];
    for color in colors {
        match color {
            Color::Red => println!("Красный"),
            Color::Green => println!("Зелёный"),
            Color::Blue => println!("Синий"),
            Color::Yellow => println!("Жёлтый"),
        }
    }
}

Этот вариант хорош, когда вам нужно разное поведение для разных вариантов.

Важное замечание

Все эти методы работают только для unit-вариантов (без данных). Если у вас варианты с полями, например:

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
}

то итерация теряет смысл, потому что каждый вариант может содержать произвольные данные. В таких случаях обычно используют match или паттерн-матчинг.

<<>>

Альтернатива: макрос enum-iterator

Кроме strum, существует более легковесный крейт enum-iterator, который делает ровно то же самое, но с меньшим количеством зависимостей:

// Cargo.toml
// [dependencies]
// enum-iterator = "2.1"

use enum_iterator::Sequence;

#[derive(Debug, Sequence)]
enum Color {
    Red,
    Green,
    Blue,
    Yellow,
}

fn main() {
    for color in enum_iterator::all::<Color>() {
        println!("{:?}", color);
    }
}

Преимущество в том, что enum-iterator не требует отдельного крейта для макросов — всё включено в один пакет. Он также поддерживает варианты с данными, используя значения по умолчанию (например, 0 для чисел или пустую строку для String).

Если вам нужно работать с перечислениями, где некоторые варианты имеют данные, и вы хотите итерацию только по "пустым" вариантам, можно комбинировать Sequence с фильтрацией через match:

use enum_iterator::Sequence;

#[derive(Debug, Sequence)]
enum Status {
    Active,
    Inactive,
    Pending(String),
}

fn main() {
    for status in enum_iterator::all::<Status>() {
        if let Status::Pending(_) = status {
            continue; // пропускаем варианты с данными
        }
        println!("{:?}", status);
    }
}

Выбор между strum и enum-iterator — дело вкуса. Первый популярнее и имеет больше возможностей (например, преобразование в строку через Display), второй — проще и быстрее подключается.