Как стать автором
Обновить

Что делает перечисления (enum) в Rust такими мощными?

Уровень сложностиСредний
Время на прочтение4 мин
Количество просмотров2.8K

На примерах попробую показать, почему enum в Rust это несколько больше, чем обычно принято считать. Рассмотрю расширенное использование enum в типовых ситуациях. Сразу забегая вперед скажу, что в рамках статьи я не затрагиваю паттерны и мэтчинг.

Первое, что приходит в голову, когда речь заходит об enum, — это идея: «А давайте заменим все константы на enum». :) Желание логичное, давайте на него посмотрим:

Было:

const STATUS_READY: u8 = 10;
const STATUS_PROCESSING: u8 = 20;
const STATUS_DONE: u8 = 30;

// Функция установки статуса
pub fn set_status(value: u8) {
  // Тут мы пишем код по изменению статуса
}

fn main() {
  // Вызов
  set_status(STATUS_PROCESSING);
  
  // Но можно и так!!! Логическая ошибка!
  set_status(40);
}

Стало:

pub enum Status {
  Ready,
  Processing,
  Done,
}

// Функция установки статуса
pub fn set_status(value: Status) {
  // Тут мы пишем код по изменению статуса
}

fn main() {
  // Вызов (только один из элементов enum и никак иначе)
  set_status(Status::Ready);
}

Мы, очевидно, приобрели типобезопасную конструкцию, которая не позволит написать set_status(15) и избавит нас от необходимости проверять аргумент функции на диапазон допустимых значений. Но, вам не кажется, что мы что-то потеряли? А потеряли мы значения констант!

enum под капотом хранит дискриминант (минимального размера, чтобы уместить все варианты), который представляет собой порядковый номер выбранного элемента enum. Rust сам определяет и размерность и значение дискриминанта, но доступ к нему можно получить только из unsafe кода (считаем, что вообще нельзя). Таким образом, дискриминант мы использовать не можем. Даже если бы и могли, нам не всегда требуются значения по порядку! Да и сам порядок не гарантирован из-за оптимизаций, применяемых в Rust.

Как бы мы могли вернуть целочисленные значения для нашего перечисления?

Типичный подход, который очень часто наблюдаю в чужом коде:

pub enum Status {
  Ready,
  Processing,
  Done
}

impl Status {
  pub fn value(&self) -> u8 {
    match self {
      Status::Ready => 10,
      Status::Processing => 20,
      Status::Done => 30
    }
  }
}

fn main() {
  // Теперь мы можем получить значение
  println!("{}", Status::Ready.value());
}

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

pub enum Status {
    Ready = 10,
    Processing = 20,
    Done = 30
}

fn main() {
    println!("{:?}", Status::Processing as u8);
}

Базовый функционал enum не позволяет создавать варианты enum из примитивов и безопасно конвертировать в примитивы. Для этого потребуется явно использовать pattern matching. Исправляет данную ситуацию крейт num_enum. Не забудьте добавить в зависимости Cargo.toml.

use num_enum::{IntoPrimitive, TryFromPrimitive};

#[repr(u8)]
#[derive(Debug, TryFromPrimitive, IntoPrimitive,)]
pub enum Status {
  Ready = 10,
  Processing = 20,
  Done = 30,
}

fn main() {
  // Теперь мы можем получить значение
  println!("{}", Status::Ready as u8);
  
  // В контексте когда Rust может определить тип, можно и так.
  // За это отвечает трейт IntoPrimitive
  let ready: u8 = Status::Ready.into();

  // Если вам нужно создать элемент enum из примитива, то за
  // это отвечает трейт TryFromPrimitive
  println!("{:?}", Status::try_from(30).unwrap());
  
  // А тут будет паника, значения 40 нет в нашем перечислении
  // Status::try_from(40).unwrap();
}

Здесь:

  • #[repr(u8)] - добавляет строгое ограничение на тип варианта

  • #[derive(Debug, TryFromPrimitive, IntoPrimitive,)] - макрос derive добавляет автоматическую реализацию указанных трейтов, в данном случае нас интересуют TryFromPrimitive и IntoPrimitive. Они добавляют методы try_from для создания варианта из примитива и into для конвертации варианта в примитив.

Где могут потребоваться значения для элементов enum?

  • Элементы enum являются битовыми масками, а вы реализуете код, который их применяет (используем as u8).

  • Вам нужно парсить бинарный протокол и удобно было бы сразу из байтов создавать элемент enum (используем try_from).

  • Вам нужно куда-то сохранять значение (в файл, в БД), а потом читать его и восстанавливать в виде элемента enum.

  • и т.д.

Как еще расширить функционал enum?

Используем макрос derive для автоматической реализации трейтов PartialEq, Eq, PartialOrd, Ord, а так же EnumIter из крейта strum_macros (использовать совместно c крейтом strum). Не забываем добавить Debug, Clone, Copy.

Что в итоге получаем?

use num_enum::{IntoPrimitive, TryFromPrimitive};
use strum_macros::EnumIter;

#[repr(u8)]
#[derive(
  Debug, 
  Clone,
  Copy,
  PartialEq, Eq,     // Реализует операции ==, !=
  PartialOrd, Ord,   // Реализует остальные операции сравнения, в т.ч. используется при сортировке
  TryFromPrimitive,
  IntoPrimitive,
  EnumIter,          // Реализует Iter для enum
)]
pub enum Status {
  Ready = 10,
  Processing = 20,
  Done = 30,
}

fn main() {
  // Получаем возможность сравнивать, 
  println!("{}", Status::Ready < Status::Done);
  println!("{}", Status::Ready != Status::Done);
  println!("{}", Status::Ready > Status::Done);
  println!("{}", Status::Ready == Status::Done);
  // и т.д

  // Получаем возможность итерироваться по элементам enum
  for status in Status::iter() {
    println!("{:?}", status);
  }
  
  // Получаем возможность сортировать статусы
  // Например: множество статусов в векторе
  let mut statuses = vec![
    Status::Done,
    Status::Ready,
    Status::Processing,
    Status::Ready,
    Status::Ready,
  ];

  // Сортировка в прямом порядке
  statuses.sort();
  // Или в обратном
  statuses.reverse();

  for status in statuses {
    println!("{:?}", status);
  }
}

Итог

Вот так легко и непринужденно, а главное написав минимум своего кода (только определение с макросом), получен enum, элементы которого:

  1. Типобезопасны

  2. Можно создавать из соответствующих примитивов: try_from()

  3. Преобразовывать в соответствующие примитивы as, into()

  4. Они имеют конкретные значения (без создания лишней функции с match)

  5. Их можно сравнивать между собой

  6. Плюс, по enum можно итерироваться!

Я уже не говорю о том, что enum подразумевает имплементацию функций (было показано в одном из примеров) и это может сделать возможности еще шире! Разве это не прекрасно?

P.S: Rust-о-гуру, покритикуйте пожалуйста!

UPD1: исправлена информация по поводу размерности дискриминанта. Спасибо @AnthonyMikh

UPD2: уточнения по минимальной версии кода для создания enum с значениями для вариантов, небольшие правки по тексту. Спасибо @redfox0

Теги:
Хабы:
Всего голосов 10: ↑8 и ↓2+6
Комментарии30

Публикации

Работа

Rust разработчик
10 вакансий

Ближайшие события