Галоўная / Артыкулы

Што кажу энумератывыя типы ў C, Java, Rust, Go і TypeScript пра проектаванне типаў

Паруцеце, як семь мов працююць з enum-амі: ад названых цэлых лягчынакоў да з’еднанняў з атрыбутамі і об’ектаў у часе выканання, і дазнаюцеся, што гэта значыць для моделіравання ў TypeScript.

Выдавец: REACTAPP.TOP17 хв чытання3847 слоў

Крыніцы: Enums Were Never Just Constants

Практычна кожныя популярныя мовы працоўкі маюць такое панявленне, як enum, і практычна кожная з іх дае яму разныя значэння. У адной мове это проста пазначка над цэлым числам, у іншай — клас з методамі, у трэцій — аб’еднанне дадзеных разных форм, а ў TypeScript это адна з некалькіх структураў на рэвэле типа, якія перавышаюць можлівасці звычнага JavaScript. Паралельны аналіз мов C, C++, Java, Rust, Go, TypeScript і Odin практычна дакладвае гэтыя разніцы і дае вам болей точны слоўнік для выбору таго, што павінен быць викорыстана ў вашам коде на TypeScript, калі значэнне можа быць толькі аднам з калькі варыянтав.

Адна слова, калькі адзінаковых проблем

Тэоретычныя апісанні простыя. Вы пераказываеце калькі імен, і кожна з іх пазначае адно з дазволеных значэнняў:

Red
Green
Blue

Это выглядае як універсальная концэпцыя, але як толькі пачынаеш супрацоўваць з разнымі рэалізацыямі, яна раскладаецца на часты:

  • У C enum пераважна розглядаецца як названая група цэлых сталых.
  • C++ застаецца ў тым жа формате, але дадаў enum class, які забезпечвае абмежэння дыяўносці і строгей типаванне.
  • У Java кожная enum-сталая стае справжнім об’ектам класу enum class, які мае поля, канстрактары, методы і нават корпус класа для кожной сталай.
  • У Rust адна тып enum можа мяць варіанты, якія храняць абсалютна разныя даны.
  • У Go няма ключавага слова enum; заместа яго выкарыстоўваюцься сталы і ідэнтыфікатор iota.
  • Enum у TypeScript компілююцца ў об’екты JavaScript, якія існуюць пад час выканання.
  • Odin навмысна залучае enum у простыя структуры і пераказвае супаўзвучныя задачы іншым функцыям, такім как наборы бітоў і масівы з індэксамі enum.

Прычына, чаму не існуе адзінаго падзеўнага значэння, заключаецца ў тым, што „enum“ на самай працоўцы ёсьць сумкой проблем, якія проста раздзеляюць адну і тую ж ключовую слова. Адмаўкі языка павінен адначасова вырашыць, якія з наступных функцыйяў гэты элемент павінен выканаць:

  • даванне імен абмежанаму набору значэнняў
  • стварэнне новага типу заместо падзеўжвання вялікага існуючага
  • прыўязка дадзеных да адзінаковых значэнняў
  • прадстаўленне калькольных можлівых станоў значэння
  • дазвола кампайлеру падтвердзіць, што ўсе варыянты былі адрабоўваны
  • компактнае прадстаўленне комбінацый флагоў
  • выбор таго, што, якім бы то не было, застаецца ў працюючым програме

Кожны язык выбірае свой сабранак функцыйяў, і гэтыя выборы многа сказваюць пра яго прыорітэты.

C: чытабельныя імены для цэлых лічб

Традыцыйны C enum ўтварае зручнасць для называння цэлочысленных сталярых значэнняў. Тип статусу можа быць адзначаны так:

enum Status {
    STATUS_OK,
    STATUS_ERROR,
    STATUS_PENDING
};

Па суті тры гэтыя назвы проста адпавядаюць спакойным цэлым числам, якія пачынаюцца з нуля:

0
1
2

Гэта можа здавацца простым, але гэта рашае справжню проблему чытаемасці. Уявіце порэўнанне з простым числам, якое знаходзіцца ў логіцы налашчэнняў:

if (status == 2) {
    ...
}

Нічога ў гэтым рядку не паведамляе перагледчыку, што значыць 2. Той жа пераказ, напісаны з викорыстаннем іменаванага сталярога значэння, сам паводзіць адпаведную інфармацыю:

if (status == STATUS_PENDING) {
    ...
}

Гэта ўсходнія праблема існавання enum: яны заменяюць «чародзкія» числы на назвы, якія несу значэнне. Яны таксама чыста падходзяць для викорыстання з switch, дзе кожны варіант чытаецца як тверджэнне пра конкрэтную сферу, а не пра арыметыку:

switch (status) {
    case STATUS_OK:
        ...
        break;

Рэшта варіантав таксама следуе той самый патэрн:

    case STATUS_ERROR:
        ...
        break;    case STATUS_PENDING:
        ...
        break;
}

Паколькі значэнняя ўсё тыя ж звычныя, прагнозаваныя цэлыя числа, перыяды ў C ідзе на лад з кодам нізкага рэвэля: яны адпавядаюць регистрам апаратуры, полям протаколаў і форматам файлаў без жадных неспадзянак.

Адносны недагодга — слабая контрольная функцыя. C не ставіцца да перыяду як да замкнутага множына, як гэта робіць Rust. Канстанты перыяду паводзяцца як цэлыя числа, зменнай перыяду часта можна прызначыць будзь-якае цэлое число без жадных працоў, а кампайляр не зобав’язан пазначаць відсутны case. Багаты кампайляры выдаюць паведамленні пра неконтрольваныя значэння перыяду ў switch, але гэта скорэй прыемнасць інструментаў, чым гарантія мовы. C23 дадаў синтаксіс для задання фіксаванага базовага типу, чым зменшыў праслойку, якую C++ закрыў ўжо раней.

Філасафію C можна падсумаваць так:

Дайце цэлым числам кращыя назвы і звярніце увагу на мінімізацыю прыладоў, якія іх абслужваюць.

Для програмавання систем такій мінімалізм частае ўсё, што вам трэба.

C++: тая ж ідея з болей строгімі правіламі

C++ успадкаваў энумератывы типы у стылі C, а пазней запрацаваў болей строгую версію — энумератывы тип з дзеямасцю:

enum class Status {
    Ok,
    Error,
    Pending
};

Гэта змена — не проста синтаксічны элемент. Энумератары энумератывага типу з дзеямасцю павінны быць указаны з іменам типу:

Status::Ok

замест таго, каб выступаць як просты ідэнтыфікатор у асоўнаем контексте:

Ok

Не менш важлівае — энумератывы тип з дзеямасцю не пераўодзіцца апошнева ў цэлычны лік. Вы не можете випадкова поравняваць Status з несувязаным Color чы працаваць з яным там, дзе трэба int, без явнага перакантавання, што пазбегае цэлага класу тых самых непазначальных памылак.

C++ таксама дазволяе явна выбраць тип зберагання:

enum class Status : uint8_t {
    Ok,
    Error,
    Pending
};

Гэта мае значэнне калі лейяут ёсць частынай кантракту, напрыклад у пакетах сеціі або структурах, якія дзеляюцца з кодам на C. Мова разлічваяе энумы без дзейскага прастору ад тых, якія задаюцца за дапамою enum class або enum struct, прычаму якія формы практыкуюць фіксаваны адпраўнай тип.

Важна зазначыць, што C++ не перадзефініюе, шта такое энум. Ён даў можлівасць кантролю над трыма аспектамі, якія з’яўляюцца ў зв’язку з яго:

  • як назывы ўтвараюць дзейскі прастор
  • насколькі строго тип аддзеляецца ад цэлах
  • як значэнне представляецца ў памяці

У сваей сутнасі гэта заўсёды ёсць спіс названых значэнняў; проста програміст можа чырвоняць яго межы адносна чэткава.

Java: энумы як частка модэлю об’ектаў

Java ўжо больш аддаляецца ад цэлах. Формат задання выглядае знайома:

enum Status {
    OK,
    ERROR,
    PENDING
}

Адзначыце, што OK, ERROR і PENDING не ўжоць числавыя аляскі. За спецыфікацыяй языка Java enum — это спецыяльны, абмежаны формат класа, пры яком усі його экземпляры заздалегідь прызначаюцца па імену. Кожная стаянка ёсць об’ектам, які ствараецца аднойчы пад час ініцыялізаціі класа, і осколькі це об’ект, ён можа зберагчы стан і поведзінку.

Стаянкі, якія несу даны

Аргумент канстрактара можа прызначыць значэнне кожной стаянцы. У гэтым прыкладзе кожная планета фіксуе свою масу:

enum Planet {
    EARTH(5.97),
    MARS(0.642);

Поле і канстрактар, якій яго запоўняе, завершаюць декларацыю:

    final double mass;    Planet(double mass) {
        this.mass = mass;
    }
}

Тепер кожная стаянка носіць свае сабецоўскія даны, і код, якому патрэбна маса планеты, чытае значэнне з поля, а не запрашвае окольны таблыцу пошуку, якая можа выйсці з синхронізацыі.

Стаянкі, якія несу поведзінку

Enum-ы таксама можу адзяваць методы, і кожны сталітэль можа запрацоўваць сваю сябе рэалізацыю. Наступны тип аперацыі перазначае абстрактны метод apply для кожнага сталітэля:

enum Operation {
    ADD {
        @Override
        int apply(int a, int b) {
            return a + b;
        }
    },

Другі сталітэль і абстрактная декларацыя выглядаюць так:

    MULTIPLY {
        @Override
        int apply(int a, int b) {
            return a * b;
        }
    };    abstract int apply(int a, int b);
}

У гэты момент enum явна ёсць класам, і гэта нячым не випадкова. Java адналяе enum-ы зі своей об’ектно-орыентаванай модэллю, таму сталітэль можа мець поля, аргументы канстрактара, спяльныя методы і перазначэння, спецыфічныя для сталітэля. Сталітэль з сабой класовым тэлам компілюецца як ананімны падклас типу enum, і ў новейшых версіях Java такі клас enum описваецца як неявная мерыжа, адтуды калекцыя падкласоў фіксуецца за дапамой декларацыі.

Такім чынам, позыцыя Java ў гэтым падзеўлі є такой:

Якщо значэнне бераецца з фіксаванага набору, чаму яго трэба обмежваць целым числам, калі ён можа быть об’ектам з сабеўным паведаннем?

За гэтым прычынам enum-і ў Java ідзяўскі падход для машын станоў, парсераў, табелей апэратараў з прыорітетамі, а таксама для механізмаў распаду на стыле стратэгій, дзе кожны варыянт знае, як выканаць свою роботу.

Калі форма дадзення залежыць ад варіянта

Enum-і ў Java падходзяць тады, калі кожная сталяйка мае аднаковы тип заздалега вяліканых дадзенняў. Іншая проблема выступае, калі кожны варыянт патрэбуе разной структуры.

Разглядзіме паведамленні ў протаколе чату:

TextMessage
ImageMessage
VideoMessage

Тэкставы паштка содержыць строку, паштка з адпраўкай з’явы мае байты з’явы і метаданы, а відэа-паштка — ўсё іншае. Тут не зміняецца значэнне поля, а сама структура дадзеных. Эмульяцыя у стылі Java, дзе кожная константа выкорыстоўвае аднаковы набор полей, паслужае слабка. Самэ гэтае ўскладненне было выкорыстана пад час проектавання эмуляцый у Rust.

Rust: эмульяцыі як знакаваныя уніяны

У Rust кожны варіант эмульяцыі можа мець свой сабскрыпт:

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(u8, u8, u8),
}

Quit не несе нічога, Move мае іменаваныя поля, Write абгортае String, а ChangeColor храніць тры байты. Для стварэння значэння выбіраецца варіант і задаюцься яго дадзеныя:

let message = Message::Move { x: 10, y: 20 };

Іншы варіант таго ж типу выглядае так:

let message = Message::Write(String::from("Hello"));

Обе выразы ствараюць об’ект типу Message, але інфармацыя ў іх мае абсалютна разны формат. Такая структура ў тэоріі типаў называецца сумным типам, а програмісты-сістэмнікі часта называюць яе аб’еднаннем з пазначкамі: пазначка фіксуе, якая варіантна форма ў дзейнасці, а даны той варіант зберагаюцца ў пэйлоадзе. Тып описвае замкнуты набор альтернатыў:

Message
 ├── Quit
 ├── Move { x, y }
 ├── Write(String)
 └── ChangeColor(r, g, b)

Пэўны парапаранне

Кампайлер знае кожную варіантну форму, і функцыя match выкарыстоўвае гэтыя знанні. Першы варіант обрабоўвае значэнне Quit:

match message {
    Message::Quit => {
        println!("Quit");
    }

Рэшта варіантаў распакоўваюць пэйлоад кожнай варіантнай формы і прызначаюць яе поля локальным іменам:

    Message::Move { x, y } => {
        println!("Move to {}, {}", x, y);
    }    Message::Write(text) => {
        println!("{}", text);
    }    Message::ChangeColor(r, g, b) => {
        println!("RGB: {}, {}, {}", r, g, b);
    }
}

Якщо выдаліць адну з эйх, праект больш не компілюецца; Rust адмовляеся працаваць з неповным match. Дакладна прычына такога становішча — тое, што дакументацыя Rust практыкуе разам выкліканне enum і патэрн-мэчынг. Дадзенне новага варіанта пазней ператворыць кожны неповны match у базе коду на памылку компілявання, яка паказывае месцы, якія трэба адкоректаваць.

Гэтае — галоўная разліка з C enum. C enum адпавядае на пытанне:

Каторая з іменаваных сталярных значэнняў адпавядае гэтаму числу?

Rust enum адпавядае на пытанне:

Каторы варіянт зараз актыўны, і які пакет даных ўзяты з яго?

Другое пытанне охопляе значна шырэй спектар дизайнаў.

Забяганне пра недзейнае становішча

Практычныя наследкіі легкая ўважаць праз рэзультаты. Аперацыя, якая можа выйсці ўспехам або зазнаць нявезення, можа быць смоделаваная як тип з двумя варіантамі:

Result
 ├── Success(value)
 └── Error(error)

Значэнне, якое можа быць прысутным, а можа і не быць, адночасна атрыбутуецца тым жа спосабам:

Option
 ├── Some(value)
 └── None

Стандартная бібліятэка Rust предстаўляе гэтыя два варыянты як Result і Option, і гэта звычныя enum-ы. Пораўняйце гэта з падходам з аднальнымі полямі, які ўжываецца ў багатых кодавых базах:

value = null
hasValue = false
error = ...

З аднальнымі полямі нічога не заважае, каб hasValue быў true, калі value ёсць null, або каб адчыненне і значэнне былі заданыя адночасна. Кожны корыстнік должен памятаць непрызначаныя правілы. З типам-сумой такія комбінацыі вообща не можуць быць створаныя, таму што ўзнікненне значэння адбываецца толькі чераз адны з прызначаных варіантов.

Это адаптаваець адна з найглыбэйшых метаў дизайну enum:

Enum можа безпасэродзе закодаваць правілы, якія стосуюцца формату вашых дадзенняў, у самай системе типаў.

Калі правілы знаходзяцца ў типе, кампайляр можа выявіць нехваткуючыя варыянты ўсё раней, чым што-небудзь запускаецца. Разработчыкі TypeScript маюць схожы аптэнат працуючы з дискримінаванымі аюніямі, дзе спяльнае літеральнае поле, такое як kind, выполняе роль тага, а перакананне never забезпечвае выключнасць.

Go: ціхаў жоднага ключавага слова enum

Пасля Rust падход Go можа здавацца крокам назад у часе. Жоднага ключавага слова enum няма. Ідыома — это блок сталярных значэнняў, якія керуецца iota:

const (
    StatusOK = iota
    StatusError
    StatusPending
)

У заявленні сталярога iota вяртае індэкс текущай сталярогай спецыфікацыі, які пачынаецца з нуля, і ён можа выступаць у выразах. Калі ў блоке пропускаецца выраз, Go павтарае пярэдні, тады:

const (
    A = iota
    B
    C
)

дае:

A = 0
B = 1
C = 2

Пакалькі неявна павтарэнне копіюе весь выраз, iota можа выконваць больш, чым проста лічэнне. Скліканне на яго дае ступені два:

const (
    Read  = 1 << iota
    Write
    Execute
)

Рэзультатныя значэнні ўсё такія:

Read    = 1
Write   = 2
Execute = 4

Разныя біты робяць гэтыя сталярогі натуральнымі флагамі: Read | Write сумавляе правы, а бітова АНД пераканальвае пра ўсё адно з іх. Таксама можна стварыць іменаваны тип, каб сталярогі не былі простае цэлае числа:

type Permission uint8

і тады заявіць сталярогі з гэтым типам:

const (
    Read Permission = 1 << iota
    Write
    Execute
)

Толькі Read адаптаваецца пад значэнне Permission, але завдзяк таму, што выраз павтараецца, ён таксама перадае тип, таму Write і Execute таксама ўважаюцца значэнням Permission. Спефікацыя Go разлічае типаваныя і нетыпаваныя константы, а іменаваны тип дае можлівасць часткова захистацца ад сумешанняя некалякіх значэнняў. Аднак гэта не стварае таго роду ідэнтыфікацыі энумерацый, якія існуюць у мовах з спецыяльным конструктам.

Гэта ўласнасць мовы Go: уместо таго, каб дадаць новую функцыю, яна з’являе гнучкую систему констант і залічваў абстракцыю вышэйшага рангу пра програміста. Цена за гэта — тое, што мова ніколі формальна не прыўязвае

these constants

до

this closed set of variants

Как гэта делае Rust чыста Java. Будзь-яка значэнне адпраўленага типу можа быць пераканвертавана ў Permission, і кампайлер не перакантролюе, чы розгляд switch гэтых стацыянарных значэнняў є выключны. Команды, якія хочуць такую гарантію, зазвычай дадаюць лінтер чыста генератор коду.

TypeScript: enum, які існуе ў часе выканання

TypeScript — гэта пераважна шар статычных типаў над JavaScript. Аляскі тыпу, такія як гэты:

type User = {
    name: string;
};

не залечыць жадных следаў у гэнераванам JavaScript; ён стыраецца падчас кампайліравання. Enums наражаюць гэтае правіло:

enum Direction {
    Up,
    Down,
    Left,
    Right
}

Звычны enum у TypeScript кампайліруецца ў рэальны об’ект JavaScript, і падрабнік TypeScript чыста ясна описвае гэтая поведзенне ў часе выканання. Для числовых enums гэнераванный об’ект уключае як прымусовыя, так і зворачныя супаработкі, таму можна перайсці з імені на число чыста з числа назад на імя:

Direction.Up    -> 0
Direction[0]    -> "Up"

Это стварае здзівленне у разработчыкаў, якія думаюць, што всё, што дадае TypeScript, прыналежы толькі перыоду кампайлявання. Це таксама мае практычныя наследкі: enum дадае код у ваш бандл, а ітераванне над числовым enum за дапамою Object.keys вяртае числовыя ключы як строкі разам з іменамі.

Строковыя enum прызначаюць явныя строкавыя значэння:

enum Direction {
    Up = "UP",
    Down = "DOWN",
    Left = "LEFT",
    Right = "RIGHT"
}

У яных няма зворачных супараднікаў, а ўсе іх значэння набагато лёгкія да чытання ў журналах, пакетах дадзеных у сеціі та выходных даных дэбагера, адтолькі што "UP" сама сабою ўсё пояснюе, тады калі 0 — няма. TypeScript таксама падтрымляе вычыслюваныя члены та const enum, якія інлайнуецца ў месцах выкарыстоўвання і адключаецца з выходных дадзеных. Зважайце, што const enum пагана супрацоўвае з інструментамі, якія кампайляюць адна файлоўка за раз, і самэ гэта ўсёлікія проекты ад яго ухіляюцца.

Альтернатыва — object-plus-as-const

Даследжэнні TypeScript таксама паказвае простейшы шаблон, створаны з звычайнага об’екта і асэрціі as const:

const Direction = {
    Up: 0,
    Down: 1,
    Left: 2,
    Right: 3,
} as const;

Это звычны JavaScript, які розумеюць усі інструменты, тады калі as const задае TypeScript літэральныя типы для кожнай власцівасці. Адпаведны аб’яднаны тип можа быць выведзены за дапамою (typeof Direction)[keyof typeof Direction]. Шаблон стаў ёш болей актуальным, калі среды выканання пачалі магчымаць безпосереднюе адключэнне типаў: такі падход адмаўляе толькі анотацыі, а энумы, якім патрэбна рэальная генерацыя коду, да яго не падходзяць. Пераканайцеся ў актуальных даследжэннях вашай среды выканання і кампайлера ў паўнай меры ўсіх правілах; стаття на що на самай працэ ўрабляе натыўны падтрымкі TypeScript у Node.js детальна розглядае гэты аспект.

Галоўная ідея заключаецца у тым, што дзейнасць enum у TypeScript выклікаеяе яго супрацоўнік — JavaScript. Java мог бы з самага пачатку запрацаваць enum як функцыю першага класу пад час выканання. TypeScript жа змушаны працаваць разам з JavaScript, у якога няма власных enum. Тады пытанне выглядае так:

У што павінна ператварыцца функцыя на рэжыме типа, калі падставны мовапрацоўнік — JavaScript?

Чыстага адказу няма, і самэ гэта прывела да таго, што у TypeScript засталося калькі перакрываючыхся варыянтаў.

Odin: просты enum плюс адносна незалежныя інструменты

Odin выбірае іншы падход. Його enum-ы залишаюцца блізкія да класычнай ідеі названага набору значэнняў, і замест таго, каб прыродзіць enum у іерархію об’ектаў або алгебраічны тип дадзэнняў, язык запрошвае аддзельныя механізмы для суседніх задач: аднародныя типы, масівы з элементамі, якія індексуюцца за дапамой enum, унііі та наборы бітов, элементны тип якіх можа быть enum.

Звычны enum описвае направлення компаса:

Direction :: enum {
    North,
    East,
    South,
    West,
}

Набор бітов, створаны на адной з эйнамаў, тады выражае будзь-яю комбінацыю направленняў:

Direction_Set :: bit_set[Direction]

Принцып дзейства — раздзел працы:

  • фіксаваны набор значэнняў — это enum
  • колекцыя гэтых значэнняў — это набор бітов
  • дадзеныя, асоцыяваныя з кожным значэнням, знаходзяцца ў масіве, індексуюцамся за дапамой enum
  • альтэрнатыўны варыянты з разнымі структурами дадзэнняў — это унія

Паколькі кожны інструмент выкаанае адзін завад, можна рассматрываць кожны з яго окрема, і ніхто з іх не павінен несці на сабе весь тыяжар іншых. Пораўняйце гэта з мовай Go, дзе флагі ствараюцца вручную за дапамою операцый сдвагу, або з мовай Rust, дзе сам enum падтрымляе як простыя лісты, так і з’еднанні з атрыбутамі.

Няма жадной мовы, якая правільна адрабатала enum

Хочацца класіфікацыя гэтых падходаў, але яны адпавядаюць на разныя запытанні. Короткае парапарэння:

  • C: іменаваныя цэлочысленныя сталярнікі, оптымізаваныя па простасці, чытаемасці і сумеснасці на низкам рэвэле.
  • C++: тая жа база з неабяжным дыапазонам дасягнем, строгейшай типаваннёй і явным адпаведным типам.
  • Java: фіксаваны набор экземпляраў класа enum, які можа мець поля, канстрактары, методы і поведзенне для кожнага сталярніка.
  • Rust: замкнутыя сэткі варіантаў, кожны з якіх можа хаваць своия даны, пры чым кампайлер забягчае або выкананне, або падтрымку ўсіх можлівых шаблонаў.
  • Go: няма спецыяльнай структуры; константы і iota генеруюць последовыя або вырахаваныя значэння.
  • TypeScript: энумы, якія ў часе выканання існуюць у вачыні об’ектаў JavaScript, на адзінаку ад большай часткі функцый типаў у TypeScript, пашыроныя таксама альтернатывамі, якія можна выдаліць.
  • Odin: простыя энумы, дапамагаемыя сэткамі бітаў, масавымі элементамі з энумерацыяй і ўніямі для адпаведных задач.
  • Ні адна з гэтых структураў не ёсць апдэйтаванай версіяю пакульнай. Кожная з іх выражае тое, каму, на думку яе разработчыкаў, павінна служыць даная функцыя.

    Што розкрывае дизайн энумаў пра мову

    Enum-ы ўсьмохненыя настолькі, што іх можна выучыць за аднаго дня, пры тым яны адкрываюць філасофію мовы, якая іх абгрунтуе. Їх можна розглядаць як запитанні, які, здаецца, ставяць сабе кожныя мовы:

    • C: як можна эфектыўна представіць гэта за дапамогою простых прымітіў?
    • C++: як зберагчы контроль на найвышэйшам роўні апаратуры і водночас дадаць болей безпечныя абстракцыі?
    • Java: што, як кожны элемент замкнутага множына будзе цэлым об’ектам з сабой прыемамі?
    • Rust: чы можа адны тип раскрасіць усе дазволеныя станы разам з іх пакетам дадзеных?
    • Go: чы воабшта патрэбна спецыяльная функцыя?
    • TypeScript: у чым павінна стаць функцыя, якая працуе ў час компілявання, калі яя павінна працаваць як звычны JavaScript?
    • Odin: чы можа основная структура застаць простай, калі спецыяльныя функцыі будуць адпаведаць супакойным патрэбам?

    Паколькі мовы оптымізуюцься пад разныя цілі, enum-ы ніколі не сталі едной ідеяй.

    Enum-ы — это рашэнні пра моделюванне, а не сынтаксіс

    Лёгкаа ўявіць enum як частку сынтаксісу:

    enum {
        A,
        B,
        C
    }
    

    Сынтаксіс — гэта найменш цікавая частка. Аслівны вопыт — гэта тое, што вы хачаце, каб система типаў гарантавала па ўсім значэнням, якія вы перыявілі:

    A, B, C
    

    Рашэнне гэтага вопыту значыць адпаведныя адпаведныя на калькольні меншых вопытаў:

    • Ці гэтыя — проста імены для цалакоў?
    • Чы хача кожнае значэнне маты супакоўаныя данні?
    • Чы кожны варіянт хача маты свою супэрфісу працы?
    • Чы кампайлер павінен ведаць, што гэтыя — ўсе дзейнасны статусы, і прыменяць ўсе можлівыя способы обробкі?
    • Чы вам патрабуюцца кампактныя флагі бітаў?
    • Чы значэнні павінны існаваць у часе выканання, для ітерацыі аб верыфікацыі?
    • Як іх павінна серыялізаваць праз канэкшн аб у сховішча?

    Разныя адказы ведуць да разных дзеянняў, а яных наследкі выходзяць за межы синтаксу: распалоўкі памяці, безпека типаў, тое, што можа аналізаваць кампайляр, выканаўскія затраты, падпараджэнне шаблонаў, формат API, формат серыявання, і, ў канцэ нас, тое, як команда спакушаеся да своих дадзеных.

    Іноды правільны enum — гэта не enum

    Гэта мае прымітныя наследкі для ўседзённага TypeScript. Язык праслужвае enum таму, што існуе певная проблема, але гэта не значыць, што кожны фіксаваны набор значэнняў павінен стаць enum. Залежна ад ситуацыі:

    • союз літералаў строкі часта ёсць яснейшы і не куплівае нічога пад час выканання
    • звычайны об’ект з as const дае значэнняі і типы пад час выканання без спецыяльнага кампайлявання
    • спецыяльны клас мае сэнс, калі кожная опцыя выконвае значныя дзеяння
    • ціфра з наборам бітоў або флагамі — гэта правы інструмент для комбінацый
  • Спецыяльны союз — гэта тое, чаго насправды трэба, калі варіянты несу розныя даны.
  • Інодзе трое добра названых сталярых значэнняў — гэта вся структура, якой заслуговы проблема.
  • Хорашая разработка типоў пачынаецца з даных і ўсіх іх властивасцяў, а не з якога-небудзь ключавага слова, яке проста є ў мове.

    Галоўныя выводы

    • „Enum“ аб’еднвае калькі незалежных аспектаў: называнне, разныя типы, прыўязаныя даны, формы варіянтов, выключнасць, флагі і наявнасць у часе выканання.
    • C і C++ сфокусаваны на цэлых лічбах і ўсуненні неяснасцей у ўпрадзе; Java — на об’ектах; Rust — на сумарных типах з перакрыцчам, якое пераглядаецца кампайлером.
    • Go і Odin паказваюць два протыпадных спосабы абходжання універсальнага enum: стварыць яго самостоятельна зі сталярых значэнняў або раздзеліць задачу між спецыялізаванымі функцыямі.
    • Enum-ы у TypeScript ўособлівыя об’екты часа выконання, што вплывае на розмір пакетаў, процес ітерацыі і сумяжнасць з інструментамі адключэння типаў; літэральныя з’еднанні і об’екты as const часта ёсць простейшым выборам.
    • Перш чым вжываць enum, спачатку вярніцеся да таго, якія гарантіі вам патрэбны. Выбраныя вам конструкцыі должны вынікаты з гэтых гарантій, а не навпакі.