Avatar ·

Как сопоставить значения перечисления (enum) с целым числом (i32, u64) в Rust?

Привет, коллеги! Я только начал изучать Rust и столкнулся с проблемой, которая кажется простой, но я не могу найти элегантного решения. У меня есть перечисление `Color`, которое описывает цвета: ```rust enum Color { Red, Green, Blue, } ``` Я хочу присвоить каждому варианту целочисленное значение, чтобы, например, `Red` соответствовал 0, `Green` — 1, `Blue` — 2. И потом мне нужно: 1. Получить число из варианта перечисления (например, из `Color::Green` получить `1`). 2. Сделать обратное преобразование: по числу (например, `2`) получить вариант `Color::Blue`. В C++ или C# я бы просто написал `(int)color` или `(Color)2`. В Python я бы использовал словарь. Но в Rust всё строже с типами. Я попробовал использовать `as`: ```rust let c = Color::Red; let n = c as i32; // Ошибка: non-primitive cast ``` Компилятор ругается: `non-primitive cast`. Я читал про `#[repr(i32)]`, но не уверен, что это правильно. И как сделать обратное преобразование? Напишите, пожалуйста, пример кода с объяснением. Заранее спасибо! P.S. Знаю, что есть крейты вроде `strum`, но хочется понять, как это сделать стандартными средствами Rust. <<>>

Проблема: приведение перечисления к целому числу и обратно

В Rust перечисления (enum) по умолчанию не являются целочисленными типами. Оператор as работает только для примитивных типов (например, i32 as u64). Чтобы сопоставить варианты перечисления с целыми числами, нужно явно указать представление в памяти с помощью атрибута #[repr(IntType)]. Рассмотрим полное решение.

Шаг 1. Объявление перечисления с целочисленным представлением

Используйте атрибут #[repr(i32)] (или u8, i64 — любой целочисленный тип). После этого каждому варианту можно явно присвоить числовое значение:

#[repr(i32)]
enum Color {
    Red = 0,
    Green = 1,
    Blue = 2,
}

Если не указать значения явно, Rust присвоит их автоматически, начиная с 0. Но для читаемости лучше указывать явно.

Шаг 2. Преобразование enum → i32 (прямое приведение)

Теперь можно использовать as:

let color = Color::Green;
let number: i32 = color as i32;
println!("{}", number); // Выведет: 1

Это работает, потому что атрибут #[repr(i32)] гарантирует, что память под перечисление — это просто i32.

Шаг 3. Преобразование i32 → enum (обратное приведение)

Прямого приведения из числа в enum в Rust нет. Нужно использовать match или if let. Самый безопасный способ — реализовать метод from_i32 (или реализовать трейт TryFrom):

impl Color {
    fn from_i32(n: i32) -> Option<Color> {
        match n {
            0 => Some(Color::Red),
            1 => Some(Color::Green),
            2 => Some(Color::Blue),
            _ => None, // Если число не соответствует ни одному варианту
        }
    }
}

// Использование:
if let Some(color) = Color::from_i32(2) {
    println!("{:?}", color); // Выведет: Blue
}

Шаг 4. Более идиоматичный способ: реализация трейта TryFrom

Стандартный способ в Rust — реализовать трейт TryFrom (или From, если преобразование гарантированно успешно):

use std::convert::TryFrom;

impl TryFrom<i32> for Color {
    type Error = &'static str;

    fn try_from(value: i32) -> Result<Self, Self::Error> {
        match value {
            0 => Ok(Color::Red),
            1 => Ok(Color::Green),
            2 => Ok(Color::Blue),
            _ => Err("Invalid color value"),
        }
    }
}

// Использование:
let color = Color::try_from(1).unwrap(); // Ok(Color::Green)

Полный пример

#[repr(i32)]
#[derive(Debug, PartialEq)]
enum Color {
    Red = 0,
    Green = 1,
    Blue = 2,
}

impl Color {
    fn from_i32(n: i32) -> Option<Color> {
        match n {
            0 => Some(Color::Red),
            1 => Some(Color::Green),
            2 => Some(Color::Blue),
            _ => None,
        }
    }
}

fn main() {
    // enum → i32
    let c = Color::Green;
    let n: i32 = c as i32;
    println!("Green = {}", n); // 1

    // i32 → enum (безопасно)
    let c2 = Color::from_i32(2).expect("Invalid value");
    println!("{:?}", c2); // Blue

    // Альтернативный синтаксис с match
    let input = 0;
    match input {
        0 => println!("Red"),
        1 => println!("Green"),
        2 => println!("Blue"),
        _ => println!("Unknown color"),
    }
}

Важные замечания

  • Безопасность: Всегда используйте Option или Result при обратном преобразовании, потому что число может не соответствовать ни одному варианту.
  • Атрибут #[repr(i32)] обязателен только для прямого приведения as i32. Если вам нужно только обратное преобразование (число → enum), можно обойтись без него, используя обычный match.
  • Производительность: Компилятор оптимизирует match до таблицы переходов (jump table), если варианты идут подряд. Это так же быстро, как приведение типов в C++.

Теперь вы умеете работать с enum как с целыми числами в Rust. Помните: явное лучше неявного, поэтому используйте TryFrom для безопасности типов.

<<>>

Альтернативный подход: использование крейта `num_enum`

Если вы не хотите писать boilerplate-код с match для каждого перечисления, можно воспользоваться крейтом num_enum. Он автоматически генерирует методы from_i32 и into_i32 через derive-макросы.

Пример

// Добавьте в Cargo.toml: num_enum = "0.7"
use num_enum::{IntoPrimitive, TryFromPrimitive};

#[derive(Debug, IntoPrimitive, TryFromPrimitive)]
#[repr(i32)]
enum Color {
    Red = 0,
    Green = 1,
    Blue = 2,
}

fn main() {
    // enum → i32
    let c = Color::Green;
    let n: i32 = c.into(); // 1

    // i32 → enum (безопасно)
    let c2 = Color::try_from(2).unwrap(); // Blue

    // Если число недопустимо:
    let result = Color::try_from(10);
    assert!(result.is_err());
}

Плюсы и минусы

  • + Минимум кода: одна строка с атрибутами.
  • + Поддержка Into, TryFrom, FromStr и других трейтов.
  • - Зависимость от внешнего крейта (хотя он легковесный и популярный).
  • - Для новичков может быть непонятно, что происходит "под капотом".

Если вы только учите Rust, рекомендую сначала освоить ручное решение с match и TryFrom. Когда почувствуете уверенность — используйте num_enum для ускорения разработки.