Как сопоставить значения перечисления (enum) с целым числом (i32, u64) в 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 для ускорения разработки.