Как в Rust перебрать все варианты перечисления (enum) в цикле?
Прямой итерации нет — используйте вспомогательные структуры
В 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), второй — проще и быстрее подключается.