На примерах попробую показать, почему 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, элементы которого:
-
Типобезопасны
-
Можно создавать из соответствующих примитивов:
try_from() -
Преобразовывать в соответствующие примитивы
as,into() -
Они имеют конкретные значения (без создания лишней функции с
match) -
Их можно сравнивать между собой
-
Плюс, по
enumможно итерироваться!
Я уже не говорю о том, что enum подразумевает имплементацию функций (было показано в одном из примеров) и это может сделать возможности еще шире! Разве это не прекрасно?
P.S: Rust-о-гуру, покритикуйте пожалуйста!
ссылка на оригинал статьи https://habr.com/ru/articles/899792/
Добавить комментарий