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

Комментарии 30

Я могу ответить на этот вопрос в одно предложение.
Перечисления в Rust делают такими мощными тип-суммы с разными типами параметров и соответствующий им pattern matching.

Ваша статья правда не совсем про это.
Но да, они реально отличные.

Да, как раз эту часть я не затрагиваю, она обычно не вызывает вопросов и хорошо описана в документации и книгах. И да, это основное, за что их любят.

Есть у enum и минусы, которые вытекают из плюсов. Если сильно увлечься вложенностью, особенно больших перечислений, то память будет использоваться не совсем оптимально (по самому большому элементу перечисления).

Да, но это в целом про многие типы сказать можно

Не согласен. Это касается именно enum, т.к. для структур память выделяется по другому. Там нет оверхеда, кроме случаев когда поля структуры сами является enum.

Представьте, что у вас enum имеет один из элементов MyEnum::Variant10(u8, u8, u8), а все остальные MyEnum::Variant0-9(u8), память будет выделена так:
1) дискриминант (по умолчанию usize т.е. 4 или 8 байт)
2) 3 байта
Итого: 11 байт на adm64

В случае с MyStruct(u8, u8, u8) все просто, это всегда 3 байта, если не брать в расчет выравнивание.

Далее, представьте:
MyEnum2::SomeVariant(MyEnum), это уже 8+11 байт! При условии, что у вас остальные MyEnum2::Variant(u8) это будет приличный оверхед по памяти, особенно если храните списки значений.

Не написал об этом в статье, но #[repr(u8)] явно изменяет размер дискриминанта и по сути позволяет задавать его вручную, сразу -7байт на amd64.

Все так, Я лишь имел в виду, что многие сложные типы имеют скрытый оверхед. Делая вложенные структуры тоже можно быстро вылезти.
Если же нас беспокоят байтовые размеры, то мы будем обращать внимание и на размер enum и на размер использующих его структу.

Представьте, что у вас enum имеет один из элементов MyEnum::Variant10(u8, u8, u8), а все остальные MyEnum::Variant0-9(u8), память будет выделена так:

  1. дискриминант (по умолчанию usize т.е. 4 или 8 байт)

  2. 3 байта Итого: 11 байт на adm64

Зачем вы дезинформируете людей? Во-первых, по умолчанию размер дискриминанта выбирается минимального размера, подходящего для хранения всех вариантов. Во-вторых, если бы бы удосужились действительно проверить этот код, то увидели бы, что он печатает 4:

enum A {
    V0(u8, u8, u8),
    V1(u8, u8, u8),
    V2(u8, u8, u8),
    V3(u8, u8, u8),
    V4(u8, u8, u8),
    V5(u8, u8, u8),
    V6(u8, u8, u8),
    V7(u8, u8, u8),
    V8(u8, u8, u8),
    V9(u8, u8, u8),
    V10(u8),
}

fn main() {
    println!("{}", std::mem::size_of::<A>());
}

А вы неправильно прочитали мой текст. Сделайте наоборот. Один вариант (u8, u8, u8), а остальные (u8). Но и тут я не учел один момент, должен быть третий размер опции. Допускаю, что я неправ, но в вашем варианте из-за одного варианта (u8), включается оптимизация, такая же как для Option<T>, т.к у вас всего 2 размера у опций. Добавьте третью и проверьте.

Размер дискриминанта Rust выбирает сам, об этом написано в статье.

В вашем случае дискриминант usize, т.е 4 байта. Вариантов по размеру всего 2, включилась оптимизация. Но это не точно )) может и u8, +3 байта. Но я все таки думаю, что дискриминант и данные будут объединены в рамках оптимизации.

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

enum A {
    V0(u8, u8, u8, u32),
    V1(u8, u8, u8),
    V2(u8, u8, u8),
    V3(u8, u8, u8),
    V4(u8, u8, u8),
    V5(u8, u8, u8),
    V6(u8, u8, u8),
    V7(u8, u8, u8),
    V8(u8, u8, u8),
    V9(u8, u8, u8),
    V10(u8),
}

enum B {
    V0(A),
    V1(u8),
    V2(u32, u32),
}

enum C {
    V0(A),
    V1(B),
    V2(A, B),
}

fn main() {
    println!("{}", std::mem::size_of::<A>()); // 4 байта
    println!("{}", std::mem::size_of::<B>()); // 8 байт
    println!("{}", std::mem::size_of::<C>()); // 20 байт
}

Во-первых, по умолчанию размер дискриминанта выбирается минимального размера, подходящего для хранения всех вариантов.

Посмотрел подробнее, это утверждение тоже не является аксиомой. В общем с оптимизациями как обычно можно запутаться. Можно было бы списать на внутреннюю реализацию Option. Но нет.

enum MyOption {
    Some(u32, u32),
    None
}

enum MyOption2 {
    Some(u64),
    None
}

fn main() {
    println!("Option<u64>={}", std::mem::size_of::<Option<u64>>()); // 16 байт
    println!("Option<(u32, u32)>={}", std::mem::size_of::<Option<(u32, u32)>>()); // Неожиданно 12 байт
    println!("MyOption={}", std::mem::size_of::<MyOption>()); // 12 байт
    println!("MyOption2={}", std::mem::size_of::<MyOption2>()); // 16 байт
}

Так всё логично. u64 должен быть выровненным по 8 байт. Поэтому размер второй структуры должен делиться на 8, содержать u64 (8 байт) и дискриминант - минимальный размер 16 байт.
u32 требует выравнивания до 4 байт. Размер первой структуры должен делиться на 4 и содержать два u32 (8 байт на пару). Минимальное число, удовлетворяющее такому свойству - 12 байт.

Согласен. Логично. Но утверждение о минимально возможном дискриминанте ломается из-за выравнивания. Тогда требуется корректировка: минимально возможный с учетом выравнивания и оптимизаций.

В данном случае минимальный это 1 бит. Впихнуть его некуда, все значения возможны -> следовательно u8 -> из-за выравнивания 8байт для u64 и 4 байта для u32.

Можно по-прежнему считать, что дискриминант минимальный.
Просто sizeof::<(u8, u32, u32)> = 12, из-за выравнивания.
Получается, что мы храним дискриминант (1 байт), паддинг (3 байта) и 2 u32 (по 4 байта).

Удивительно, как вот такой код просто для создания enum с самыми базовыми возможностями у кого то вызывает ощущение "мощности" языка:

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

А у кого то ощущение непродуманности и невероятного оверинжиниринга.

В TypeScript у enum это все доступно из коробки, и намного удобней.

Даже, если абстрагироваться от того, что назначение у языков разное и корректнее было бы сравнивать скажем с C17, C++20, C++23, попробуйте посмотреть с другой точки зрения.

В случае когда весь возможный функционал отключён и дается простой инструментарий для его подключения (в данном случае через макросы), где больше спектр возможных решений? Очевидно, что в Rust! Ответьте на следующий вопрос: можете вы в TypeScript для enum запретить сортировку элементов, запретить их сравнивать, запретить по ним итерироваться? Или комбинация: итерироваться можно, а сравнивать и сортировать нет.

Или другая аналогия. У вас есть два веб-сервера, для одного есть конфиг с настройками, а другой конфига не имеет, но работает на максимуме возможностей из коробки. Что функциональней?

азначение у языков разное и корректнее было бы сравнивать скажем с C17, C++20, C++23

Само по себе назначение не должно влиять вообще ни на что. Важно только можно так сделать или нет, и на что это фактически влияет.

можете вы в TypeScript для enum запретить сортировку элементов, запретить их сравнивать, запретить по ним итерироваться? Или комбинация: итерироваться можно, а сравнивать и сортировать нет.

Аналог троллейбуса из буханки, смешно))

но работает на максимуме возможностей из коробки

Вы хотите сказать что добавление таких возможностей замедлит код или что? Можно было так и написать. Вот только почему то я уверен, что это все можно реализовать вообще без доп нагрузки, было бы интересно послушать доводы.

Само по себе назначение не должно влиять вообще ни на что. Важно только можно так сделать или нет, и на что это фактически влияет.

Я так и предложил делать без оглядки на разное назначение ЯП.

Аналог троллейбуса из буханки, смешно))

Спор с формальной логикой. Вариантов возможных решений при применении enum в Rust больше. Это применимо к любому объекту реальной жизни. Более функционален тот, который дает больше вариантов при принятии решения.

Вы хотите сказать что добавление таких возможностей замедлит код или что? Можно было так и написать. Вот только почему то я уверен, что это все можно реализовать вообще без доп нагрузки, было бы интересно послушать доводы.

Конкретно в Rust, добавление или отключение на итоговую производительность не влияют. В TypeScript enum компильнется в JS-объект, который будет минимум x2 в памяти из-за прямого и обратного мапинга. Вот вам и плата за дефолтное наличие значений у enum! На этом шаге за вас уже решили и try_from вам сделали. И что еще самое интересное, по другому и не сделать! Даже если убрать обратный мапинг, значения у элементов enum должны остаться, что тоже будет оверхедом в случае когда они не нужны. Уже не будем говорить, что имена элементов enum это строки

Спор с формальной логикой

Логика может быть в корне неверной. Язык, в котором нет ничего лишнего, лучше, чем в котором лишнее есть. Язык,в котором можно сделать что то только единственным способом - самым правильным, лучше, чем тот, в котором способов много [и все плохие].

Если кому то понадобиться тот бред, что вы перечислили, то пусть он и запрещает явно все эти базовые вещи. Но повторюсь, никому это нафиг никогда нужно не будет, поэтому по умолчанию все это должно быть.

Конкретно в Rust, добавление или отключение на итоговую производительность не влияют. 

Если оно не влияет, и учитывая первый пункт про "по умолчанию должно быть", то это просто др***ево на пустом месте - результат непродуманности языка.

Вот это логика, а то что у вас это слабая попытка в логику.

Вы включили эмоции вместо логики и утверждаете, что это логика :-)

Язык, в котором нет ничего лишнего...

Искажаете. Я говорил не об этом. Есть всё, но по умолчанию выключено, хотите включайте, хотите не включайте.

Видимо недопоняли. То, что написано в статье это не типичный вариант кода, скорее "иногда требуется". Если проанализируете репосы с кодом Rust, то увидите, что в большинстве случаев значениями для enum вообще не пользуются (чаще используют match), не сортируют и не сравнивают их друг с другом. Но есть класс задач, где требуется.

Язык,в котором можно сделать что то только единственным способом - самым правильным

Всерьез о правильности реализации enum в TypeScript? Скорее это компромис! У авторов компилятора просто нет другого способа реализовать это в JS. Я сейчас сделаю enum поверх HashMap и буду утверждать, что это самая лучшая и правильная реализация. Зато в ней все из коробки %)

Компромис - это вообще самое частое слово в IT ))

Если кому то понадобиться тот бред, что вы перечислили

Так бы и написали, что у вас не было таких задач. Человеку сложно представить то, с чем он не сталкивался. Именно поэтому для вас это "бред".

Если оно не влияет, и учитывая первый пункт про "по умолчанию должно быть"

Посмотрите основной фукнционал enum в Rust. Он не описан в статье. В качестве эксперимента попробуйте создать в TypeScript enum такой же как Message:

enum MessageError {
    Validation,
    Size,
    Unknown(String)
}

enum Message {
    Quit,
    MoveTo { x: u32, y: u32 },
    Error(MessageError),
    Text(String),
}

Всерьез о правильности реализации enum в TypeScript

Я ни разу не утверждал то, что в TS именно реализация идеальная - речь про синтаксис и встроенные возможности, без необходимости костылей. Переводите тему - чувствуете слабую позицию.

В качестве эксперимента попробуйте создать в TypeScript enum такой же как Message

В TypeScript и это сделано куда лучше - через Union Type. И будет выглядеть так:

type MessageError =
 | "validation"
 | "size"
 | { type: "unknown", message: string }


type Message =
  | { type: "quit" }
  | { type: "move-to", x: number, y: number }
  | { type: "error", error: MessageError }
  | { type: "text", text: string }

И можно спокойно объединять уже имеющиеся типы без создания дурацких енумов.

Опять, уходите от темы про непродуманность, предлагая спор про совсем другое.

Я пожалуй удаляюсь. Позицию высказал довольно четко в прошлых сообщениях.

Что сложного вы увидели? Есть структура данных, есть определения которые описывают, как она должна себя вести в различных ситуациях. Всего 2 (две) концепции. Альтернатива этому - выдумывать поведение "по дефолту", а потом удивляться, что что-то пошло не так. Сами имена трейтов даже не нужно помнить, компилятор сам в ошибке скажет, что нужно.

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

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

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

Да, это минимальный вариант кода, который это делает. В статье как раз об этом.

Неужели? Этого нет в статье.

Ну как же. В 4ом блоке кода используется [#repr(u8)] и два варианта преобразования в т.ч as u8

В примере уже используются IntoPrimitive.

Да, но он дает только .into()
Сейчас правки судя по всему временно не возможны из-за технотекста. Как будет доступно, я внесу пояснения. На самом деле просто очень часто встречаю в чужом коде матчинг вместо указания значений в enum, что выглядит странно т.к. значения были с самой первой версии.

Можно даже короче, но без гарантий представления как u8

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

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

И так

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

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

А вот так уже будет ошибка компиляции

#[repr(u8)]
pub enum Status {
    Ready = 10,
    Processing = 20,
    Done = 256
}

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

Тоже интересное поведение. Можно подумать, что Done будет себя вести как u16, но это не так.

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

fn main() {
    println!("{:?}", Status::Done as u8); // 0
    // как ведет себя варинт Done при приведении
    println!("{:?}", (256 & 0xFF) as u8); // 0
    //println!("{:?}", 256 as u8); // ошибка компиляции
}

Оператор as довольно опасный:

#![deny(clippy::as_conversions)]

#[repr(u16)]
pub enum Status {
    Ready = 10,
    Processing = 20,
    Done = 256,
}

impl From<Status> for u16 {
    #[allow(clippy::as_conversions)]
    fn from(item: Status) -> Self {
        item as _
    }
}

fn main() {
    // println!("{}", u8::from(Status::Done));
    println!("{}", u16::from(Status::Done));
}

Тут согласен. Rust Playground, кстати, забивает на #![deny(clippy::as_conversions)]

// println!("{}", u8::from(Status::Done));

Тут в другом проблема, From<Status> for u8 не реализован. Наверное имелось ввиду Status::Done as u8

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации