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

от автора

На примерах попробую показать, почему 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 под капотом хранит дискриминант (обычно 4-х байтный), который представляет собой порядковый номер выбранного элемента 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()); }

Решение рабочее, но не очень элегантное.

Исправляет данную ситуацию крейт 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(); }

Где могут потребоваться значения для элементов 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-о-гуру, покритикуйте пожалуйста!


ссылка на оригинал статьи https://habr.com/ru/articles/899792/