Avatar ·

Как создать перечисления с константными значениями в Rust?

Привет, коллеги! Я начал изучать Rust после C++ и Python, и столкнулся с неожиданной для меня проблемой: как правильно создавать перечисления (enums) с константными значениями? В C++ я привык писать что-то вроде `enum Color { RED = 0xFF0000, GREEN = 0x00FF00, BLUE = 0x0000FF };`, и каждый вариант имел числовое значение. В Python я использовал `IntEnum` или просто классы с константами. Но в Rust, когда я пытаюсь сделать так: ```rust enum HttpStatus { Ok = 200, NotFound = 404, InternalServerError = 500, } ``` Компилятор выдает ошибку, если я пытаюсь использовать эти значения как целые числа. Например, я хочу преобразовать статус в число для отправки по сети или для логирования. Пробовал писать `status as u32`, но это работает только для перечислений, которые явно наследуют целочисленный тип. А если я хочу хранить строки или другие сложные данные? Например, для перечисления "ошибки" с кодами и текстовыми описаниями: ```rust enum Error { NotFound { code: 42, message: "Not Found" }, Timeout { code: 1, message: "Request timed out" }, } ``` Это вообще не компилируется. Я в тупике. Как в Rust принято решать такие задачи? Нужно ли использовать какие-то крейты или есть встроенные механизмы? Объясните, пожалуйста, для новичка, с примерами. Заранее спасибо! <<>>

Проблема: перечисления в Rust — это не просто числа

В Rust перечисления (enum) — это мощный инструмент алгебраических типов данных, а не просто набор именованных констант, как в C/C++. Каждый вариант перечисления может содержать любые данные: числа, строки, структуры, другие перечисления. Это даёт гибкость, но требует иного подхода к "константным значениям".

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

#[repr(u32)]
enum HttpStatus {
    Ok = 200,
    NotFound = 404,
    InternalServerError = 500,
}

fn main() {
    let status = HttpStatus::NotFound;
    let code = status as u32; // 404
    println!("Status code: {}", code);
}

Ключевой момент — атрибут #[repr(u32)]. Он указывает, что каждый вариант хранится как 32-битное целое. Без этого атрибута приведение через as работать не будет. Но такой подход работает только для целых чисел.

Что делать, если нужны строки или сложные данные?

Для вашего второго примера с ошибками (код + сообщение) нужно использовать другой паттерн — данные внутри вариантов. Rust позволяет хранить кортежи, структуры или даже другие перечисления прямо в варианте:

#[derive(Debug, Clone)]
enum Error {
    NotFound { code: u32, message: String },
    Timeout { code: u32, message: String },
    Unknown { code: u32, message: String },
}

impl Error {
    // Конструкторы для удобства
    fn not_found() -> Self {
        Error::NotFound {
            code: 42,
            message: String::from("Not Found"),
        }
    }

    fn timeout() -> Self {
        Error::Timeout {
            code: 1,
            message: String::from("Request timed out"),
        }
    }

    // Метод для получения кода
    fn code(&self) -> u32 {
        match self {
            Error::NotFound { code, .. } => *code,
            Error::Timeout { code, .. } => *code,
            Error::Unknown { code, .. } => *code,
        }
    }

    // Метод для получения сообщения
    fn message(&self) -> &str {
        match self {
            Error::NotFound { message, .. } => message,
            Error::Timeout { message, .. } => message,
            Error::Unknown { message, .. } => message,
        }
    }
}

fn main() {
    let err = Error::not_found();
    println!("Error {}: {}", err.code(), err.message());
}

Более идиоматичный подход: крейты и константы

Если вы хотите именно "константные" значения (как в C++), можно использовать крейт strum или встроенные константы. Вот пример с константами:

// Вариант 1: ассоциированные константы
enum Color {
    Red,
    Green,
    Blue,
}

impl Color {
    const RED_HEX: u32 = 0xFF0000;
    const GREEN_HEX: u32 = 0x00FF00;
    const BLUE_HEX: u32 = 0x0000FF;

    fn hex(&self) -> u32 {
        match self {
            Color::Red => Self::RED_HEX,
            Color::Green => Self::GREEN_HEX,
            Color::Blue => Self::BLUE_HEX,
        }
    }
}

// Вариант 2: структура вместо перечисления (если нужны только константы)
struct HttpStatus;
impl HttpStatus {
    const OK: u32 = 200;
    const NOT_FOUND: u32 = 404;
}

fn main() {
    println!("Red in hex: {:#X}", Color::Red.hex());
    println!("OK status: {}", HttpStatus::OK);
}

Крейт strum (рекомендую для сложных случаев)

Добавьте в Cargo.toml зависимость: strum = { version = "0.26", features = ["derive"] }. Затем:

use strum::{EnumString, Display, IntoStaticStr};

#[derive(Debug, EnumString, Display, IntoStaticStr)]
enum ErrorCode {
    #[strum(serialize = "ERR_NOT_FOUND", to_string = "Not Found")]
    NotFound = 42,
    #[strum(serialize = "ERR_TIMEOUT", to_string = "Timeout")]
    Timeout = 1,
}

fn main() {
    let err = ErrorCode::NotFound;
    println!("Code: {}", err as i32); // 42 (работает, если добавить #[repr(i32)])
    println!("String: {}", err); // "Not Found"
    let static_str: &'static str = err.into(); // "ERR_NOT_FOUND"
}

Главный вывод: в Rust нет прямого аналога C-стиля "enum с произвольными значениями". Вместо этого используйте:

  • #[repr(тип)] для целочисленных констант;
  • Варианты с данными для сложных значений;
  • Ассоциированные константы или крейты (strum, num_enum) для удобства.

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

<<>>

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

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

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

num_enum = "0.7"

Пример использования:

use num_enum::{IntoPrimitive, TryFromPrimitive};

#[derive(Debug, Clone, Copy, IntoPrimitive, TryFromPrimitive)]
#[repr(u32)]
enum HttpStatus {
    Ok = 200,
    NotFound = 404,
    InternalServerError = 500,
}

fn main() {
    // Из перечисления в число
    let status = HttpStatus::NotFound;
    let code: u32 = status.into(); // 404
    println!("Code: {}", code);

    // Из числа в перечисление (с обработкой ошибки)
    match HttpStatus::try_from(500u32) {
        Ok(s) => println!("Status: {:?}", s),
        Err(_) => println!("Invalid code"),
    }

    // Если уверены, что число валидно — используйте try_from().unwrap()
    let status_ok = HttpStatus::try_from(200).unwrap();
    assert_eq!(status_ok as u32, 200);
}

Этот подход избавляет от ручного написания match и делает код чище. Для строковых констант лучше использовать strum, как описано в первом ответе.