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