Go: предложение: спецификация: добавить поддержку типизированного перечисления

Созданный на 1 апр. 2017  ·  180Комментарии  ·  Источник: golang/go

Я хотел бы предложить, чтобы перечисление было добавлено в Go как особый вид type . Приведенные ниже примеры заимствованы из примера protobuf.

Enums в Go сегодня

type SearchRequest int
var (
    SearchRequestUNIVERSAL SearchRequest = 0 // UNIVERSAL
    SearchRequestWEB       SearchRequest = 1 // WEB
    SearchRequestIMAGES    SearchRequest = 2 // IMAGES
    SearchRequestLOCAL     SearchRequest = 3 // LOCAL
    SearchRequestNEWS      SearchRequest = 4 // NEWS
    SearchRequestPRODUCTS  SearchRequest = 5 // PRODUCTS
    SearchRequestVIDEO     SearchRequest = 6 // VIDEO
)

type SearchRequest string
var (
    SearchRequestUNIVERSAL SearchRequest = "UNIVERSAL"
    SearchRequestWEB       SearchRequest = "WEB"
    SearchRequestIMAGES    SearchRequest = "IMAGES"
    SearchRequestLOCAL     SearchRequest = "LOCAL"
    SearchRequestNEWS      SearchRequest = "NEWS"
    SearchRequestPRODUCTS  SearchRequest = "PRODUCTS"
    SearchRequestVIDEO     SearchRequest = "VIDEO"
)

// IsValid has to be called everywhere input happens, or you risk bad data - no guarantees
func (sr SearchRequest) IsValid() bool {
    switch sr {
        case SearchRequestUNIVERSAL, SearchRequestWEB...:
            return true
    }
    return false
}

Как это может выглядеть с языковой поддержкой

enum SearchRequest int {
    0 // UNIVERSAL
    1 // WEB
    2 // IMAGES
    3 // LOCAL
    4 // NEWS
    5 // PRODUCTS
    6 // VIDEO
}

enum SearchRequest string {
    "UNIVERSAL"
    "WEB"
    "IMAGES"
    "LOCAL"
    "NEWS"
    "PRODUCTS"
    "VIDEO"
}

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

  • Безопасность для экспортируемых типов : ничто не мешает кому-то сделать SearchRequest(99) или SearchRequest("MOBILEAPP") . Текущие обходные пути включают создание неэкспортируемого типа с параметрами, но это часто затрудняет использование / документирование полученного кода.
  • Безопасность во время выполнения : точно так же, как protobuf проверяет правильность при демаршалинге, это обеспечивает проверку на уровне языка в любое время, когда создается экземпляр перечисления.
  • Инструментарий/документация : сегодня многие пакеты помещают допустимые параметры в комментарии к полям, но не все это делают, и нет гарантии, что комментарии не устарели.

Что следует учитывать

  • Nil : реализовав enum поверх системы типов, я не думаю, что для этого потребуется специальный корпус. Если кто-то хочет, чтобы nil был действительным, то перечисление должно быть определено как указатель.
  • Значение по умолчанию/назначения во время выполнения : это одно из самых сложных решений. Что, если значение Go по умолчанию не определено как допустимое перечисление? Статический анализ может частично смягчить это во время компиляции, но должен быть способ обработки внешнего ввода.

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

Go2 LanguageChange NeedsInvestigation Proposal

Самый полезный комментарий

@ md2perpe , это не перечисление.

  1. Их нельзя перечислить, повторить.
  2. У них нет полезного строкового представления.
  3. У них нет личности:

```иди
основной пакет

импорт (
"ФМТ"
)

основная функция () {
введите SearchRequest int
константа (
Универсальный поисковый запрос = йота
Интернет
)

const (
    Another SearchRequest = iota
    Foo
)

fmt.Println("Should be false: ", (Web == Foo))
    // Prints: "Should be false:  true"

}
````

Я полностью согласен с @derekperkins в том, что Go нуждается в некотором перечислении как гражданине первого класса. Как это будет выглядеть, я не уверен, но подозреваю, что это можно сделать, не разбивая стеклянный дом Go 1.

Все 180 Комментарий

@derekparker , есть обсуждение внесения предложения Go2 в # 19412

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

перечисления — это особый случай типов суммы, где все типы одинаковы, и с каждым из них связано значение методом. Конечно, больше печатать, но тот же эффект. Независимо от того, будет ли это одно или другое, типы сумм охватывают больше, и даже типы сумм маловероятны. В любом случае до Go2 ничего не происходит из-за соглашения о совместимости с Go1, поскольку эти предложения, по крайней мере, потребуют нового ключевого слова, если какое-либо из них будет принято.

Достаточно справедливо, но ни одно из этих предложений не нарушает соглашение о совместимости. Было высказано мнение, что типы сумм «слишком велики», чтобы добавлять их в Go1. Если это так, то это предложение представляет собой ценную золотую середину, которая может стать ступенькой к типам с полной суммой в Go2.

Им обоим требуется новое ключевое слово, которое сломает действительный код Go1, используя его в качестве идентификатора.

Я думаю, что это можно было бы обойти

Новой языковой функции нужны убедительные варианты использования. Все возможности языка полезны, иначе никто бы их не предлагал; вопрос в том, достаточно ли они полезны, чтобы оправдать усложнение языка и необходимость изучения новых концепций всеми? Каковы убедительные варианты использования здесь? Как люди будут их использовать? Например, ожидают ли люди, что они смогут перебирать набор допустимых значений перечисления, и если да, то как они это сделают? Делает ли это предложение что-то большее, чем просто позволяет избежать добавления вариантов по умолчанию для некоторых коммутаторов?

Вот идиоматический способ написания перечислений в текущем Go:

type SearchRequest int

const (
    Universal SearchRequest = iota
    Web
    Images
    Local
    News
    Products
    Video
)

Это имеет то преимущество, что легко создавать флаги, которые могут быть объединены по ИЛИ (используя оператор | ):

type SearchRequest int

const (
    Universal SearchRequest = 1 << iota
    Web
    Images
    Local
    News
    Products
    Video
)

Я не вижу, чтобы введение ключевого слова enum сделало бы его намного короче.

@ md2perpe , это не перечисление.

  1. Их нельзя перечислить, повторить.
  2. У них нет полезного строкового представления.
  3. У них нет личности:

```иди
основной пакет

импорт (
"ФМТ"
)

основная функция () {
введите SearchRequest int
константа (
Универсальный поисковый запрос = йота
Интернет
)

const (
    Another SearchRequest = iota
    Foo
)

fmt.Println("Should be false: ", (Web == Foo))
    // Prints: "Should be false:  true"

}
````

Я полностью согласен с @derekperkins в том, что Go нуждается в некотором перечислении как гражданине первого класса. Как это будет выглядеть, я не уверен, но подозреваю, что это можно сделать, не разбивая стеклянный дом Go 1.

@md2perpe iota — это очень ограниченный способ обращения к перечислениям, который отлично подходит для ограниченного набора обстоятельств.

  1. Вам нужен int
  2. Вам нужно только быть последовательным внутри вашего пакета, а не представлять внешнее состояние

Как только вам нужно представить строку или другой тип, что очень характерно для внешних флагов, iota вам не подойдет. Если вы хотите сопоставить внешнее представление/представление базы данных, я бы не стал использовать iota , потому что тогда порядок в исходном коде имеет значение, а изменение порядка может вызвать проблемы с целостностью данных.

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

@ianlancetaylor

Например, ожидают ли люди, что они смогут перебирать набор допустимых значений перечисления, и если да, то как они это сделают?

Я думаю, что это надежный вариант использования, как упоминал @bep. Я думаю, что итерация будет выглядеть как стандартный цикл Go, и я думаю, что они будут повторяться в том порядке, в котором они были определены.

for i, val := range SearchRequest {
...
}

Если бы Go добавил что-то большее, чем йота, почему бы тогда не использовать алгебраические типы данных?

Расширяя порядок в соответствии с порядком определения и следуя примеру protobuf, я думаю, что значение поля по умолчанию будет первым определенным полем.

@bep Не так удобно, но вы можете получить все эти свойства:

package main

var SearchRequests []SearchRequest
type SearchRequest struct{ name string }
func (req SearchRequest) String() string { return req.name }

func Request(name string) SearchRequest {
    req := SearchRequest{name}
    SearchRequests = append(SearchRequests, req)
    return req
}

var (
    Universal = Request("Universal")
    Web       = Request("Web")

    Another = Request("Another")
    Foo     = Request("Foo")
)

func main() {
    fmt.Println("Should be false: ", (Web == Foo))
    fmt.Println("Should be true: ", (Web == Web))
    for i, req := range SearchRequests {
        fmt.Println(i, req)
    }
}

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

  • перечисления, проверенные во время компиляции, не совместимы ни назад, ни вперед в случае добавления или удаления. #18130 тратит значительные усилия на переход к постепенному восстановлению кода; перечисления уничтожили бы это усилие; любой пакет, который когда-либо захочет изменить набор перечислений, автоматически и принудительно сломает все их импортеры.
  • Вопреки тому, что утверждает исходный комментарий, protobuf (по этой конкретной причине) фактически не проверяет действительность полей перечисления. proto2 указывает, что неизвестное значение для перечисления следует рассматривать как неизвестное поле, а proto3 даже указывает, что сгенерированный код должен иметь способ представить их с закодированным значением (точно так же, как в настоящее время go делает с fake-enums)
  • В конце концов, это на самом деле не добавляет много. Вы можете получить стрингификацию, используя инструмент стрингера. Вы можете получить итерацию, добавив дозорный MaxValidFoo const (но см. предостережение выше. У вас даже не должно быть требования). У вас просто не должно быть двух const-decls в первую очередь. Просто интегрируйте в свой CI инструмент, который это проверяет.
  • Я не верю, что на самом деле необходимы другие типы, кроме целых. Инструмент стрингера уже должен охватывать преобразование в струны и обратно; в конце концов, сгенерированный код будет эквивалентен тому, что в любом случае сгенерирует компилятор (если вы серьезно не предполагаете, что любое сравнение «string-enums» будет перебирать байты…)

В общем, просто огромный -1 для меня. Он не только ничего не добавляет; активно болит.

Я думаю, что текущая реализация enum в Go очень проста и обеспечивает достаточную проверку времени компиляции. На самом деле я ожидаю каких-то перечислений Rust с базовым сопоставлением с образцом, но это, возможно, нарушает гарантии Go1.

Поскольку перечисления являются частным случаем типов сумм, а общепринятое мнение состоит в том, что мы должны использовать интерфейсы для имитации типов сумм, ответ очевиден: https://play.golang.org/p/1BvOakvbj2

(если непонятно: да, это шутка — в классической программистской манере я ошибся на единицу).

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

Как и инструмент стрингера, инструмент «рейнджер» может генерировать эквивалент функции Iter в коде, который я привел выше.

Что-то может сгенерировать реализации {Binary,Text}{Marshaler,Unmarshaler}, чтобы упростить их отправку по сети.

Я уверен, что есть много таких мелочей, которые были бы весьма полезны при случае.

Существуют некоторые инструменты проверки/линтера для проверки полноты типов сумм, смоделированных с помощью интерфейсов. Нет причин, по которым не может быть перечислений iota, которые сообщают вам, когда пропущены регистры или используются недопустимые нетипизированные константы (может быть, он должен просто сообщать что-то кроме 0?).

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

Перечисления дополнят уже установленную систему типов. Как показали многие примеры в этом выпуске, строительные блоки для перечислений уже присутствуют. Точно так же, как каналы являются абстракциями высокого уровня, основанными на большем количестве типов примитивов, перечисления должны быть построены таким же образом. Люди высокомерны, неуклюжи и забывчивы, такие механизмы, как перечисления, помогают программистам-людям делать меньше ошибок при программировании.

@bep Я не согласен со всеми тремя вашими пунктами. Идиоматические перечисления Go сильно напоминают перечисления C, которые не имеют итерации допустимых значений, не имеют автоматического преобразования в строки и не обязательно имеют различную идентичность.

Хорошо иметь итерацию, но в большинстве случаев, если вам нужна итерация, можно определить константы для первого и последнего значений. Вы даже можете сделать это таким образом, чтобы не требовалось обновление при добавлении новых значений, поскольку iota автоматически сделает это последним. Ситуация, когда языковая поддержка будет иметь существенное значение, — это когда значения перечисления не являются смежными.

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

Наконец, индивидуальная идентификация, я даже не уверен, что это вообще полезная функция. Перечисления не являются типами суммы, как, скажем, в Haskell. Они называются числами. Например, использование перечислений в качестве значений флагов является распространенным явлением. Например, у вас может быть ReadWriteMode = ReadMode | WriteMode , и это полезная вещь. Вполне возможно также иметь и другие значения, например, у вас может быть DefaultMode = ReadMode . Ни один метод не может помешать кому-то написать const DefaultMode = ReadMode в любом случае; какой цели служит требование, чтобы это происходило в отдельном объявлении?

@bep Я не согласен со всеми тремя вашими пунктами. Идиоматические перечисления Go сильно напоминают перечисления C, которые не имеют итерации допустимых значений, не имеют автоматического преобразования в строки и не обязательно имеют различную идентичность.

@alercah , пожалуйста, не втягивайте это idomatic Go в какую-либо дискуссию как предположительно «выигрышный аргумент»; В Go нет встроенных Enum, поэтому говорить о каких-то несуществующих идомах не имеет смысла.

Go был создан, чтобы быть лучше C/C++ или менее многословной Java , поэтому сравнение его с последним было бы более разумным. И в Java есть встроенный Enum type («Типы перечисления языка программирования Java намного мощнее, чем их аналоги в других языках».): https://docs.oracle.com/javase/tutorial/java /javaOO/enum.html

И, хотя вы можете не согласиться с «гораздо более мощной частью», тип Java Enum обладает всеми тремя упомянутыми выше функциями.

Я могу оценить аргумент о том, что Go компактнее, проще и т. д., и что необходимо пойти на некоторый компромисс, чтобы сохранить его таким, и я видел несколько хакерских обходных путей в этой ветке, которые вроде работают, но набор iota целые числа не сами по себе составляют перечисление.

Перечисления и автоматические преобразования строк являются хорошими кандидатами на использование функции "go generate". У нас уже есть некоторые решения. Перечисления Java — это что-то среднее между классическими перечислениями и типами сумм. Так что, на мой взгляд, это плохой языковой дизайн.
Дело в идиоматичности Go — ключевая, и я не вижу веских причин копировать все фичи из языка X в язык Y только потому, что кто-то с ним знаком.

Типы enum языка программирования Java намного мощнее, чем их аналоги в других языках.

Это было верно десятилетие назад. Посмотрите современную бесплатную реализацию Option в Rust, основанную на типах суммы и сопоставлении с образцом.

Дело в идиоматичности Go — ключевая, и я не вижу веских причин копировать все фичи из языка X в язык Y только потому, что кто-то с ним знаком.

Обратите внимание, что я не слишком не согласен с приведенными здесь выводами, но использование _идиоматического Go_ ставит Go на какой-то вычурный пьедестал. Программирование большинства программ довольно скучно и практично. И часто вам просто нужно заполнить раскрывающийся список перечислением...

//go:generate enumerator Foo,Bar
Написано один раз, доступно везде. Обратите внимание, что пример является абстрактным.

@bep Я думаю, вы неправильно прочитали исходный комментарий. Предполагалось, что «идиоматические перечисления Go» относятся к текущей конструкции с использованием типа Foo int + const-decl + iota, я полагаю, а не для того, чтобы сказать «все, что вы предлагаете, не является идиоматическим».

@rsc Что касается метки Go2 , это противоречит моим доводам в пользу подачи этого предложения. #19412 — это предложение о полной сумме типов, которое является более мощным надмножеством, чем мое простое предложение перечисления здесь, и я бы предпочел увидеть его в Go2. С моей точки зрения, вероятность того, что Go2 произойдет в ближайшие 5 лет, ничтожно мала, и я бы предпочел, чтобы что-то произошло в более короткие сроки.

Если мое предложение о новом зарезервированном ключевом слове enum невозможно для BC, есть еще другие способы его реализации, будь то полная языковая интеграция или инструменты, встроенные в go vet . Как я уже говорил, я не особо разбираюсь в синтаксисе, но я твердо верю, что это было бы ценным дополнением к Go сегодня, не добавляя значительной когнитивной нагрузки для новых пользователей.

Новое ключевое слово невозможно до Go 2. Это было бы явным нарушением гарантии совместимости с Go 1.

Лично я пока не вижу веских аргументов в пользу enum или, если на то пошло, типов суммы, даже для Go 2. Я не говорю, что этого не может быть. Но одна из целей языка Go — простота языка. Недостаточно, чтобы языковая функция была полезной; все возможности языка полезны — если бы они не были полезными, никто бы их не предлагал. Чтобы добавить функцию в Go, у функции должно быть достаточно привлекательных вариантов использования, чтобы стоило усложнять язык. Наиболее привлекательные варианты использования — это код, который нельзя написать без этой функции, по крайней мере, сейчас без большой неловкости.

Я хотел бы видеть перечисления в Go. Я постоянно обнаруживаю, что хочу ограничить свой открытый API (или работать с ограниченным API вне моего приложения), в котором есть ограниченное количество допустимых входных данных. Для меня это идеальное место для перечисления.

Например, я мог бы создать клиентское приложение, которое подключается к какому-то API в стиле RPC и имеет указанный набор действий/кодов операций. Я могу использовать const для этого, но ничто не мешает кому-либо (включая меня!) просто отправить неверный код.

С другой стороны, если я пишу серверную часть для того же API, было бы неплохо иметь возможность написать оператор switch для перечисления, который вызовет ошибку компилятора (или, по крайней мере, несколько go vet предупреждения), если не проверены все возможные значения перечисления (или хотя бы существует default: ).

Я думаю, что это (enums) область, в которой Swift действительно разобрался правильно .

Я мог бы создать клиентское приложение, которое подключается к какому-то API в стиле RPC и имеет указанный набор действий/кодов операций. Я могу использовать для этого константы, но ничто не мешает кому-либо (включая меня!) просто отправить неверный код.

Это ужасная идея для решения с перечислениями. Это означало бы, что теперь вы никогда не сможете добавить новое значение перечисления, потому что внезапно RPC могут дать сбой или ваши данные станут нечитаемыми при откате. Причина, по которой proto3 требует, чтобы сгенерированный enum-код поддерживал значение «неизвестный код», заключается в том, что это урок, усвоенный болью (сравните его с тем, как proto2 решил это, что лучше, но все же очень плохо). Вы хотите, чтобы приложение могло корректно обрабатывать этот случай.

@Merovius Я уважаю ваше мнение, но вежливо не согласен. Убедиться, что используются только допустимые значения, — одно из основных применений перечислений.

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

Конечно, для работы с внешними процессами состояние «о-о-о» необходимо.

С перечислениями (или более общими и полезными типами сумм) вы можете добавить явный «неизвестный» код к сумме/перечислению, с которым компилятор заставляет вас иметь дело (или просто обрабатывать эту ситуацию полностью в конечной точке, если все, что вы можете сделать, это зарегистрируйте его и перейдите к следующему запросу).

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

За границами API вариантов использования меньше, и всегда следует ошибаться в сторону расширяемости, но иногда у вас действительно есть что-то, что действительно может быть только одной из X вещей, например, с AST или более тривиальными примерами, такими как «день значение недели», где диапазон в значительной степени установлен на данный момент (вплоть до выбора календарной системы).

@jimmyfrasche Я могу дать вам день недели, но не AST. Грамматика развивается. То, что может быть недействительным сегодня, может быть полностью действительным завтра, и это может потребовать добавления новых типов узлов в AST. С проверенными компилятором типами суммы это было бы невозможно без поломок.

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

Я играю с реализацией клиента для серверного API. Некоторые аргументы и возвращаемые значения являются перечислениями в API. Всего существует 45 типов перечислений.

Использование перечислимых констант в Go в моем случае невозможно, поскольку некоторые значения для разных типов перечислений имеют одно и то же имя. В приведенном ниже примере Destroy появляется дважды, поэтому компилятор выдает ошибку Destroy redeclared in this block .

type TaskAllowedOperations int
const (
    _ TaskAllowedOperations = iota
    Cancel
    Destroy
)

type OnNormalExit int
const (
    _ OnNormalExit = iota
    Destroy
    Restart
)

Следовательно, мне нужно будет придумать другое представление. В идеале тот, который позволяет IDE отображать возможные значения для данного типа, чтобы пользователям клиента было легче его использовать. Наличие enum в качестве гражданина первого класса в Go удовлетворило бы это.

@kongslund Я знаю, что это не идеальная реализация, но я только что сделал генератор кода, который может вас заинтересовать. Требуется только, чтобы вы объявили свое перечисление в комментарии над объявлением типа, а остальное сгенерирует для вас.

// ENUM(_, Cancel, Destroy)
type TaskAllowedOperations int

// ENUM(_, Destroy, Restart)
type OnNormalExit int

Будет генерировать

const(
  _ TaskAllowedOperations = iota
  TaskAllowedOperationsCancel
  TaskAllowedOperationsDestroy
)

const(
  _ OnNormalExit = iota
  OnNormalExitDestroy
  OnNormalExitRestart
)

Лучше всего то, что он будет генерировать методы String() , которые исключают в них префикс, что позволяет вам анализировать "Destroy" либо как TaskAllowedOperations , либо как OnNormalExit .

https://github.com/abice/go-enum

Теперь, когда штекер не в пути...

Я лично не возражаю, что перечисления не включены в состав языка go, что не было моим первоначальным мнением по этому поводу. Когда я впервые собирался уходить, у меня часто была смущенная реакция на то, почему было сделано так много вариантов. Но после использования языка приятно иметь простоту, которой он придерживается, и если что-то дополнительное необходимо, велика вероятность, что это нужно кому-то еще, и он сделал отличный пакет, чтобы помочь с этой конкретной проблемой. Оставляю количество крафта на свое усмотрение.

В этом обсуждении было поднято много важных вопросов, некоторые в пользу поддержки enum, а также многие против нее (по крайней мере, насколько в предложении говорилось что-либо о том, что такое "enums" в первую очередь). Несколько вещей, которые запомнились мне:

  • Вводный пример (Enums в Go сегодня) вводит в заблуждение: этот код сгенерирован, и почти никто не стал бы писать такой код Go вручную. На самом деле предложение (как это могло бы выглядеть с языковой поддержкой) намного ближе к тому, что мы на самом деле уже делаем в Go.

  • @jediorange упоминает, что Swift «действительно понял (перечисления) правильно»: как бы то ни было, но перечисления Swift - это удивительно сложный зверь, смешивающий все виды концепций вместе. В Go мы сознательно избегали механизмов, которые пересекались с функциями других языков, и взамен получали большую ортогональность. Следствием для программиста является то, что ему не нужно принимать решение, какую функцию использовать: перечисление или класс, или тип суммы (если они у нас есть), или интерфейс.

  • К мнению @ianlancetaylor о полезности языковых функций нельзя относиться легкомысленно. Есть огромное количество полезных функций; вопрос в том, какие из них действительно убедительны и стоят своих денег (дополнительной сложности языка и, следовательно, удобочитаемости, а также реализации).

  • В качестве второстепенного момента, константы, определенные йотой, в Go, конечно же, не ограничены целыми числами. Пока они являются константами, они ограничены (возможно, именованными) базовыми типами (включая числа с плавающей запятой, логические значения, строки: https://play.golang.org/p/lhd3jqqg5z).

  • @merovius хорошо отмечает ограничения (статических!) проверок во время компиляции. Я очень сомневаюсь, что перечисления, которые нельзя расширить, подходят в ситуациях, когда расширение желательно или ожидается (любая долгоживущая поверхность API со временем развивается).

Это подводит меня к некоторым вопросам об этом предложении, на которые, я считаю, необходимо ответить, прежде чем будет достигнут какой-либо значимый прогресс:

1) Каковы фактические ожидания от предложенных перечислений? @bep упоминает перечислимость, итерируемость, строковые представления, идентичность. Есть ли еще? Есть меньше?

2) Предполагая список в 1), можно ли расширить перечисления? Если да, то как? (в том же пакете? в другом пакете?) Если их нельзя расширить, то почему? Почему это не проблема на практике?

3) Пространство имен: в Swift тип перечисления вводит новое пространство имен. Существует значительный механизм (синтаксический сахар, вывод типов), так что имя пространства имен не должно повторяться везде. Например, для значений перечисления перечисления Month в правильном контексте можно написать .January, а не Month.January (или, что еще хуже, MyPackage.Month.January). Нужно ли пространство имен enum? Если да, то как расширяется пространство имен enum? Какой синтаксический сахар требуется, чтобы это работало на практике?

4) Являются ли значения enum константами? Неизменяемые значения?

5) Какие операции возможны над значениями enum (скажем, помимо итерации): Могу ли я переместиться на одну вперед, одну назад? Требуются ли дополнительные встроенные функции или операторы? (Не все итерации могут быть в порядке). Что произойдет, если кто-то продвинется вперед за последнее значение перечисления? Это ошибка времени выполнения?

(Я исправил формулировку следующего абзаца в https://github.com/golang/go/issues/19814#issuecomment-322771922. Приносим извинения за небрежный выбор слов ниже.)

Без попытки ответить на эти вопросы это предложение бессмысленно («Я хочу, чтобы перечисления делали то, что я хочу» — это не предложение).

Без попытки ответить на эти вопросы это предложение бессмысленно.

@griesemer У вас есть отличный набор вопросов / вопросов, но называть это предложение бессмысленным из-за того, что вы не отвечаете на эти вопросы, не имеет большого смысла. В этом проекте установлена ​​высокая планка для вклада, но должно быть разрешено _предлагать что-то_, не имея докторской степени в компиляторах, и предложение не должно быть _готовым к реализации_ дизайна.

  • Go нуждался в этом предложении, так как оно положило начало столь необходимой дискуссии => ценность и значение.
  • Если это также приведет к предложению # 21473 => значение и смысл

Вводный пример (Enums в Go сегодня) вводит в заблуждение: этот код сгенерирован, и почти никто не стал бы писать такой код Go вручную. На самом деле предложение (как это могло бы выглядеть с языковой поддержкой) намного ближе к тому, что мы на самом деле уже делаем в Go.

@griesemer Я должен не согласиться. Я не должен был оставлять полное заглавное имя переменной Go, но есть много мест, где написанный от руки код выглядит почти идентично моему предложению, написанному сотрудниками Google, которых я уважаю в сообществе Go. Мы довольно часто следуем одному и тому же шаблону в нашей кодовой базе. Вот пример, взятый из библиотеки Google Cloud Go.

// ACLRole is the level of access to grant.
type ACLRole string

const (
    RoleOwner  ACLRole = "OWNER"
    RoleReader ACLRole = "READER"
    RoleWriter ACLRole = "WRITER"
)

Они используют одну и ту же конструкцию в нескольких местах.
https://github.com/GoogleCloudPlatform/google-cloud-go/blob/master/bigquery/table.go#L78 -L116
https://github.com/GoogleCloudPlatform/google-cloud-go/blob/master/storage/acl.go#L27 -L49

Позже было некоторое обсуждение того, как вы можете сделать вещи более краткими, если вы в порядке, используя iota , что может быть полезно само по себе, но для ограниченного варианта использования. Смотрите мой предыдущий комментарий для более подробной информации. https://github.com/golang/go/issues/19814#issuecomment-290948187

@bep Честная точка зрения; Прошу прощения за неосторожный выбор слов. Позвольте мне попробовать еще раз, надеюсь, на этот раз сформулировав мой последний абзац более уважительно и яснее:

Чтобы иметь возможность добиться значимого прогресса, я считаю, что сторонники этого предложения должны попытаться быть более точными в отношении того, что они считают важными функциями перечислений (например, ответив на некоторые вопросы в https://github. com/golang/go/issues/19814#issuecomment-322752526). Из обсуждения до сих пор желаемые функции описаны довольно расплывчато.

Возможно, в качестве первого шага было бы действительно полезно иметь тематические исследования, которые показывают, как существующий Go отстает (значительно) и как перечисления решают проблему лучше/быстрее/понятнее и т. д. См. также отличный доклад @rsc на Gophercon. относительно изменений языка Go2.

@derekperkins Я бы назвал эти (типизированные) определения констант, а не перечисления. Я предполагаю, что наше разногласие связано с разным пониманием того, что такое «перечисление», отсюда и мои вопросы выше.

(Мои предыдущие https://github.com/golang/go/issues/19814#issuecomment-322774830 должны были быть отправлены @derekperkins , конечно, а не @derekparker. Автозаполнение победило меня.)

Судя по комментарию @derekperkins и частично отвечая на мои собственные вопросы, я понимаю, что «перечисление» Go должно обладать как минимум следующими качествами:

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

Это звучит правильно? Если да, то что еще нужно добавить в этот список?

Все ваши вопросы хороши.

Каковы фактические ожидания от предложенных перечислений? @bep упоминает перечислимость, итерируемость, строковые представления, идентичность. Есть ли еще? Есть меньше?

Предполагая список в 1), можно ли расширить перечисления? Если да, то как? (в том же пакете? в другом пакете?) Если их нельзя расширить, то почему? Почему это не проблема на практике?

Я не думаю, что перечисления могут быть расширены по двум причинам:

  1. Перечисления должны представлять полный диапазон допустимых значений, поэтому их расширение не имеет смысла.
  2. Точно так же, как обычные типы Go не могут быть расширены во внешних пакетах, это поддерживает ту же механику и ожидания разработчиков.

Пространство имен: в Swift тип перечисления вводит новое пространство имен. Существует значительный механизм (синтаксический сахар, вывод типов), так что имя пространства имен не должно повторяться везде. Например, для значений перечисления перечисления Month в правильном контексте можно написать .January, а не Month.January (или, что еще хуже, MyPackage.Month.January). Нужно ли пространство имен enum? Если да, то как расширяется пространство имен enum? Какой синтаксический сахар требуется, чтобы это работало на практике?

Я понимаю, как возникло пространство имен, поскольку во всех примерах я упоминал префикс с именем типа. Хотя я не возражал бы, если бы кто-то решительно высказался за добавление пространства имен, я думаю, что это выходит за рамки данного предложения. Префикс прекрасно вписывается в текущую систему.

Являются ли значения перечисления константами? Неизменяемые значения?

Я бы подумал константы.

Какие операции возможны над значениями enum (скажем, помимо итерации): Могу ли я переместиться на одну вперед, одну назад? Требуются ли дополнительные встроенные функции или операторы? (Не все итерации могут быть в порядке). Что произойдет, если кто-то продвинется вперед за последнее значение перечисления? Это ошибка времени выполнения?

Я бы по умолчанию использовал стандартные методы Go для срезов/массивов (не карт). Значения перечисления будут повторяться в зависимости от порядка объявления. Как минимум, будет поддержка диапазона. Я отказываюсь разрешать доступ к перечислениям через индекс, но не испытываю к этому особых чувств. Отсутствие поддержки должно устранить потенциальную ошибку времени выполнения.

Возникнет новая ошибка времени выполнения (паника?), вызванная присвоением недопустимого значения перечислению, будь то путем прямого присвоения или приведения типов.

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

  • они также определяют тип перечисления, связанный со значениями перечисления (в противном случае они просто константы) в том же объявлении
  • невозможно привести/создать значение перечисления существующего типа перечисления вне его объявления
  • их можно перебрать

Это звучит правильно? (Это соответствовало бы классическому подходу языков к перечислениям, впервые предложенному Паскалем около 45 лет назад).

Да, именно это я и предлагаю.

Как насчет операторов switch? AIUI, который является одним из основных драйверов предложения.

Я думаю, что возможность включить перечисление подразумевается, поскольку вы можете включить практически все что угодно. Мне нравится, что у Swift есть ошибки, если вы не полностью удовлетворили перечисление в своем переключателе, но с этим может справиться ветеринар.

@jediorange Я специально имел в виду вопрос последней части о том, должна ли быть проверка на полноту (в интересах сохранения полноты предложения). «Нет» — это, конечно, прекрасный ответ.

В исходном сообщении этого выпуска в качестве мотиватора упоминаются protobufs. Я хотел бы явно указать, что с семантикой, которая дана сейчас, компилятору protobuf потребуется создать дополнительный «нераспознанный» случай для любого перечисления (подразумевая некоторую схему искажения имен для предотвращения коллизий). Также потребуется добавить дополнительное поле в любую сгенерированную структуру с использованием перечислений (опять же, каким-то образом исказив имена), если декодированное значение перечисления не находится в скомпилированном диапазоне. Так же, как это делается в настоящее время для java . Или, что более вероятно, продолжать использовать int s.

@Merovius В моем первоначальном предложении протобуфы упоминались в качестве примера, а не в качестве основного мотиватора предложения. Вы поднимаете хороший вопрос об этой интеграции. Я думаю, что к этому, вероятно, следует относиться как к ортогональной проблеме. Большая часть кода, который я видел, преобразует сгенерированные типы protobuf в структуры уровня приложения, предпочитая использовать их внутри. Для меня было бы разумным, если бы protobuf мог остаться без изменений, и если создатели приложений захотят преобразовать их в перечисление Go, они могли бы справиться с крайними случаями, которые вы поднимаете в процессе преобразования.

@derekperkins Еще несколько вопросов:

  • Каково нулевое значение переменной типа enum, которая не инициализирована явно? Я предполагаю, что он вообще не может быть равен нулю (что усложняет выделение/инициализацию памяти).

  • Можем ли мы выполнить ограниченную арифметику со значениями перечисления? Например, в Паскале (на котором я программировал когда-то давно) на удивление часто приходилось выполнять итерацию с шагом > 1. А иногда требовалось вычислить значение перечисления.

  • Что касается итерации, почему поддержка итерации (и строки) с помощью go generate недостаточно хороша?

Каково нулевое значение переменной типа enum, которая не инициализирована явно? Я предполагаю, что он вообще не может быть равен нулю (что усложняет выделение/инициализацию памяти).

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

Можем ли мы выполнить ограниченную арифметику со значениями перечисления? Например, в Паскале (на котором я программировал когда-то давно) на удивление часто приходилось выполнять итерацию с шагом > 1. А иногда требовалось вычислить значение перечисления.

Используете ли вы числовые или строковые перечисления, означает ли это, что все перечисления имеют неявный нулевой индекс? Причина, по которой я упоминал ранее, что я склоняюсь только к поддерживаемым итерациям range , а не к индексам, заключается в том, что не раскрывается базовая реализация, которая может использовать массив, карту или что-то еще внизу. Я не предполагаю, что вам понадобится доступ к перечислениям через индекс, но если у вас есть причины, по которым это было бы полезно, я не думаю, что есть причина запрещать это.

Что касается итерации, почему поддержка итерации (и строки) с помощью go generate недостаточно хороша?

Лично для меня итерация не является основным вариантом использования, хотя я думаю, что она повышает ценность предложения. Если бы это было движущим фактором, возможно, go generate было бы достаточно. Это не помогает гарантировать безопасность стоимости. Аргумент Stringer() предполагает, что необработанное значение будет iota или int или каким-либо другим типом, представляющим «настоящее» значение. Вам также нужно будет сгенерировать (Un)MarshalJSON , (Un)MarshalBinary , Scanner/Valuer и любые другие методы сериализации, которые вы можете использовать, чтобы гарантировать, что значение Stringer использовалось для связи с все, что Go использует внутри.

@griesemer Думаю, я не полностью ответил на ваш вопрос о расширяемости перечислений, по крайней мере, в отношении добавления/удаления значений. Возможность редактировать их является неотъемлемой частью этого предложения.

От @Merovius https://github.com/golang/go/issues/19814#issuecomment -290969864

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

Я не вижу, чем это отличается от любого другого критического изменения API. Создатель пакета должен уважительно относиться к BC, точно так же, как если бы изменялись типы, функции или сигнатуры функций.

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

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

@derekperkins Большинство других изменений API можно организовать для постепенного исправления. Я очень рекомендую прочитать описание Расса Кокса, оно многое проясняет.

Открытые перечисления (например, текущая конструкция const+iota) допускают постепенное исправление, например: а) определение нового значения без его использования, б) обновление обратных зависимостей для обработки нового значения, в) начало использования значения. Или, если вы хотите удалить значение, а) прекратите использовать значение, б) обновите обратные зависимости, чтобы не упоминать значение, которое нужно удалить, в) удалите значение.

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

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

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

@ianlancetaylor Я определенно не смогу говорить о полной реализации, но если бы перечисления были реализованы как массив на основе 0 (это то, за что, похоже, выступает @griesemer ), то 0, поскольку индекс кажется это может удвоиться как «все биты-ноль».

С закрытыми (проверенными компилятором на полноту) перечислениями это невозможно.

@Merovius Если бы полнота была проверена с помощью go vet или аналогичного инструмента, предложенного @jediorange , по сравнению с принудительным компилятором, разве это уменьшило бы ваши опасения?

@derekperkins Насчет их вредоносности, да. Не об их бесполезности. Те же проблемы с перекосом версий также возникают в большинстве случаев использования, для которых они обычно рассматриваются (системные вызовы, сетевые протоколы, форматы файлов, общие объекты…). Есть причина, по которой proto3 требует открытых перечислений, а proto2 нет — это урок, извлеченный из многих сбоев и инцидентов с повреждением данных. Даже несмотря на то, что Google уже достаточно осторожен, чтобы избежать перекоса версий. С моей точки зрения, открытые перечисления с вариантами по умолчанию — это правильное решение. И, кроме предполагаемой защиты от недопустимых значений, они на самом деле мало что дают, насколько я могу судить.

При всем при этом я не принимаю решения.

@derekperkins В https://github.com/golang/go/issues/19814#issuecomment -322818206 вы подтверждаете, что (с вашей точки зрения):

  • объявление перечисления объявляет тип перечисления вместе с именованными значениями перечисления (константами)
  • их можно перебрать
  • никакие значения не могут быть добавлены в перечисление вне его объявления
    И позже: переключение перечислений должно быть (или, возможно, не может быть) исчерпывающим (кажется менее важным)

В https://github.com/golang/go/issues/19814#issuecomment -322895247 вы говорите, что:

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

А в https://github.com/golang/go/issues/19814#issuecomment -322903714 вы говорите, что «возможность редактировать их является важной частью этого предложения».

Я в замешательстве: так что итерация не является основным мотиватором, хорошо. Это оставляет как минимум объявление перечисления значений перечисления, которые являются константами, и они не могут быть расширены за пределы объявления. Но теперь вы говорите, что возможность редактировать их важна. Что это значит? Конечно, не то, чтобы их можно было расширить (это было бы противоречием). Являются ли они переменными? (Но тогда они не константы).

В https://github.com/golang/go/issues/19814#issuecomment -322903714 вы говорите, что перечисления могут быть реализованы как массив на основе 0. Это предполагает, что объявление перечисления вводит новый тип вместе с упорядоченным списком имен перечислений, которые представляют собой постоянные индексы, отсчитываемые от 0, в массиве значений перечисления (для которых пространство резервируется автоматически). Это то, что ты имеешь в виду? Если да, то почему было бы недостаточно просто объявить массив фиксированного размера и список индексов констант для него? Проверка границ массива автоматически гарантирует, что вы не можете «расширить» диапазон перечисления, и итерация уже будет возможна.

Что мне не хватает?

Я в замешательстве: так что итерация не является основным мотиватором, хорошо.

У меня есть свои причины, по которым я хочу перечисления, а также пытаюсь принять во внимание то, что другие в этой теме, включая @bep и других, выразили как необходимые части предложения.

Это оставляет как минимум объявление перечисления значений перечисления, которые являются константами, и они не могут быть расширены за пределы объявления. Но теперь вы говорите, что возможность редактировать их важна. Что это значит? Конечно, не то, чтобы их можно было расширить (это было бы противоречием). Являются ли они переменными? (Но тогда они не константы).

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

В #19814 (комментарий) вы говорите, что перечисления могут быть реализованы как массивы, основанные на 0.

Это просто мои предположения, помимо моей зарплаты, о том, как я представляю, как это может быть реализовано за кулисами, на основе вашего https://github.com/golang/go/issues/19814#issuecomment -322884746 и https: @ianlancetaylor : //github.com/golang/go/issues/19814#issuecomment -322899668

«Можем ли мы выполнять ограниченные арифметические действия со значениями перечисления? Например, в Паскале (на котором я когда-то программировал когда-то) на удивление часто приходилось выполнять итерацию с шагом > 1. А иногда хотелось вычислить значение перечисления».

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

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

Опять же, я не знаю, как работает компилятор, поэтому просто пытался продолжить разговор. В конце концов, я не пытаюсь предложить что-то радикальное. Как вы упоминали ранее: «Это будет соответствовать классическому подходу языков к перечислениям, впервые предложенному Паскалем около 45 лет назад», и это отвечает всем требованиям.

Всем, кто проявил интерес, пожалуйста, не стесняйтесь скинуться.

Другой вопрос заключается в том, можно ли использовать эти перечисления для индексации массивов или срезов. Срез часто является очень эффективным и компактным способом представления отображения enum->value, и я думаю, что требование карты было бы неудачным.

@derekperkins Хорошо, я беспокоюсь, что это возвращает нас (или, по крайней мере, меня) к исходной точке: какую проблему вы пытаетесь решить? Вам просто нужен более приятный способ написать то, что мы сейчас делаем с константами и, возможно, с йотой (и для чего мы используем go generate для получения строковых представлений)? То есть какой-то синтаксический сахар для обозначения, которое вы (возможно) находите чрезмерно обременительным? (Это хороший ответ, просто пытаюсь понять.)

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

В нынешнем виде у всех немного разное понимание того, что влечет за собой это предложение («перечисления»), как стало ясно из различных ответов: существует огромный диапазон возможностей между перечислениями Pascal и перечислениями Swift. Если вы (или кто-то другой) не опишете очень четко то, что предлагается (заметьте, я не прошу о реализации), будет трудно добиться какого-либо значимого прогресса или даже просто обсудить достоинства этого предложения.

Имеет ли это смысл?

@griesemer В этом есть смысл, и я понимаю, какую планку нужно преодолеть, о которой @rsc говорил на Gophercon. У вас, очевидно, гораздо более глубокое понимание, чем у меня когда-либо будет. В #21473 вы упомянули, что йота для vars не была реализована, потому что в то время не было убедительного варианта использования. Это та же самая причина, по которой enum не было включено с самого начала? Мне было бы очень интересно узнать ваше мнение о том, принесет ли это пользу Go, и если да, то с чего бы вы начали этот процесс?

@derekperkins Что касается вашего вопроса в https://github.com/golang/go/issues/19814#issuecomment -323144075: в то время (в дизайне Go) мы рассматривали только относительно простые (скажем, Pascal или C-стиль) перечисления. Я не помню всех подробностей, но определенно возникло ощущение, что от дополнительных механизмов, необходимых для перечислений, пользы недостаточно. Мы чувствовали, что они, по сути, были прославленными постоянными декларациями.

Есть также проблемы с этими традиционными перечислениями: с ними можно выполнять арифметические действия (это просто целые числа), но что это значит, если они выходят «за пределы диапазона (перечисления)»? В Go они просто константы, и «вне диапазона» не существует. Другим является итерация: в Паскале были специальные встроенные функции (я думаю, SUCC и PRED) для продвижения значения переменной типа enum вперед и назад (в C просто выполняются ++ или --). Но та же проблема возникает и здесь: что произойдет, если кто-то пойдет дальше конца (очень распространенная проблема в цикле for, перебирающем значения перечисления с использованием ++ или эквивалента Pascal SUCC). Наконец, объявление перечисления вводит новый тип, элементами которого являются значения перечисления. У этих значений есть имена (те, которые определены в объявлении enum), но эти имена (в Паскале, C) находятся в той же области, что и тип. Это немного неудовлетворительно: при объявлении двух разных перечислений можно было бы надеяться, что можно использовать одно и то же имя значения перечисления для каждого типа перечисления без конфликтов, что невозможно. Конечно, Go не решает и этого, но объявление константы также не выглядит так, будто оно вводит новое пространство имен. Лучшее решение состоит в том, чтобы ввести пространство имен для каждого перечисления, но тогда каждый раз, когда используется значение перечисления, его нужно уточнять именем типа перечисления, что раздражает. Swift решает эту проблему, выводя тип перечисления, где это возможно, а затем можно использовать имя значения перечисления с префиксом точки. Но это совсем немного техники. И, наконец, иногда (часто в общедоступных API) необходимо расширить объявление перечисления. Если это невозможно (у вас нет кода), возникает проблема. С константами таких проблем не существует.

Вероятно, это еще не все; это только то, что приходит на ум. В конце концов мы решили, что лучше эмулировать перечисления в Go, используя ортогональные инструменты, которые у нас уже были: пользовательские целочисленные типы, которые делают ошибочные присваивания менее вероятными, и механизм iota (и возможность оставлять повторяющиеся выражения инициализации) для синтаксического сахара.

Отсюда мои вопросы: что вы хотите получить от специализированных объявлений enum, которые мы не можем адекватно эмулировать в Go с небольшими синтаксическими накладными расходами? Я могу думать о перечислении и типе перечисления, который не может быть расширен за пределы объявления. Я могу придумать больше возможностей для значений перечисления, как в Swift.

Перечисление можно легко решить с помощью генератора go в Go. У нас уже есть стрингер. Ограничение расширения проблематично за пределами API. Дополнительные возможности для значений перечисления (скажем, как в Swift) кажутся очень непохожими на Go, потому что смешивают множество ортогональных концепций. В Go мы, вероятно, достигли бы этого, используя элементарные строительные блоки.

@griesemer Спасибо за ваш вдумчивый ответ. Я не согласен с тем, что они в основном являются прославленными постоянными декларациями. Наличие безопасности типов в Go — это здорово, и главная ценность, которую enum предоставил бы лично мне, — это безопасность значений. Способ имитировать это в Go сегодня — запускать функции проверки в каждой точке входа для этой переменной. Это многословно и позволяет легко делать ошибки, но это возможно с современным языком. Я уже создал пространство имен, добавив префикс имени типа перед перечислением, что, хотя и многословно, не имеет большого значения.

Мне лично не нравится большинство вариантов использования iota . Хотя это круто, большую часть времени мои enum -подобные значения сопоставляются с внешними ресурсами, такими как db или внешний API, и я предпочитаю быть более явным, чтобы значение не менялось, если вы меняете порядок. iota также не помогает в большинстве мест, где я бы использовал enum , потому что я буду использовать список строковых значений.

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

Я думаю, что канонический способ перечисления Go сегодня (как показано на https://github.com/golang/go/issues/19814#issuecomment-290909885) довольно близок к правильному.
Есть несколько недостатков:

  1. Их нельзя повторять
  2. У них нет строкового представления
  3. Клиенты могут вводить фиктивные значения перечисления

Я в порядке без № 1.
go:generate + stringer можно использовать для #2. Если это не подходит для вашего варианта использования, сделайте базовый тип вашего «enum» строкой вместо int и используйте строковые постоянные значения.

3 сложно справиться с сегодняшним Go. У меня есть глупое предложение, которое могло бы справиться с этим хорошо.

Добавьте ключевое слово explicit в определение типа. Это ключевое слово запрещает преобразования в этот тип, кроме преобразований в константных блоках в пакете, в котором определен этот тип. (Или restricted ? Или, может быть, enum означает explicit type ?)

Повторно используя пример, на который я ссылался выше,

//go:generate stringer -type=SearchRequest
explicit type SearchRequest int

const (
    Universal SearchRequest = iota
    Web
    Images
    Local
    News
    Products
    Video
)

Внутри блока const есть конверсии из int в SearchRequest . Но только автор пакета может вводить новое значение SearchRequest и вряд ли введет его случайно (например, передав int функции, которая ожидает SearchRequest ).

Я не очень активно предлагаю это решение, но я думаю, что не-случайно-создать-недействительный-один является существенным свойством перечислений, которые не могут быть захвачены в Go сегодня (если вы не перейдете к структуре с неэкспортированный маршрут поля).

Я думаю, что интересный риск с перечислениями связан с нетипизированными константами. Люди, которые пишут явное преобразование типов, знают, что они делают. Я бы хотел рассмотреть способ запрещения явных преобразований типов в Go при определенных обстоятельствах, но думаю, что это полностью ортогонально понятию перечисляемых типов. Это идея применима к любому типу.

Но нетипизированные константы могут привести к случайному и неожиданному созданию значения типа, чего нельзя сказать о явных преобразованиях типов. Поэтому я думаю, что предложение @ randall77 о explicit можно упростить, просто означая, что нетипизированные константы не могут быть неявно преобразованы в тип. Всегда требуется явное преобразование типа.

Я был бы готов рассмотреть способ для Go запретить явное преобразование типов при определенных обстоятельствах.

@ianlancetaylor Необязательное запрещение преобразований типов, явных или неявных, решит проблемы, которые в первую очередь заставили меня создать это предложение. Тогда только исходный пакет сможет создать и, таким образом, удовлетворить любые типы. Это даже лучше, чем решение enum , поскольку оно поддерживает не только объявления const , но и любой тип.

@ randall77 , @ianlancetaylor Как заставить предложение explicit работать вместе с нулевым значением этого типа?

@derekperkins Полный запрет преобразования типов сделает невозможным использование этих типов в универсальных кодировщиках/декодерах, таких как пакеты encoding/* .

@Merovius Я думаю, что @ianlancetaylor предлагает ограничение только для неявных преобразований (например, присвоение нетипизированной константы ограниченному типу). Явное преобразование по-прежнему возможно.

@griesemer Я знаю :) Но я понял, что @derekperkins предлагает другое.

Разве разрешение явных преобразований не подрывает саму причину, по которой мы думаем об этом «явном» квалификаторе? Если кто-то решит преобразовать произвольное значение в «явный» тип, то у нас будет не больше гарантий того, что данное значение является одной из перечисленных констант, чем сейчас.

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

Я полагаю, я задаюсь вопросом, соответствует ли запрет явных преобразований «духу Go». Запрет явных преобразований делает большой шаг к программированию на основе типов, а не программированию на основе написания кода. Я думаю, что Go занимает четкую позицию в пользу последнего.

@griesemer @Merovius Я снова опубликую цитату @ianlancetaylor , так как это было его предложение, а не мое.

Я был бы готов рассмотреть способ для Go запретить явное преобразование типов при определенных обстоятельствах.

И @rogpeppe, и @Merovius поднимают хорошие вопросы о разветвлениях. Разрешение явных преобразований, но не неявных преобразований, не решает проблему гарантии допустимых типов, но потеря универсального кодирования была бы довольно большим недостатком.

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

  1. Перечисления основаны только на базовых типах (string, uint, int, rune и т. д.). Если базовый тип не нужен, по умолчанию он может быть uint?
  2. Все допустимые значения перечисления должны быть объявлены с объявлением типа --Constants. Недопустимые (не объявленные в объявлении типа) значения не могут быть преобразованы в тип enum.
  3. Автоматическое строковое представление для отладки (приятно иметь).
  4. Во время компиляции проверяет полноту операторов switch в перечислении. При желании порекомендуйте (через go vet ?) случай default , даже если он уже исчерпывающий (вероятно, ошибка) для будущих изменений.
  5. Нулевое значение должно быть по существу недопустимым (не что-то в объявлении перечисления). Я лично хотел бы, чтобы это было nil , как это делает срез.

Последнее _may_ может быть немного спорным. И я не знаю наверняка, сработает ли это, но я думаю, что это будет соответствовать семантике — так же, как если бы кто-то проверял срез nil , можно было бы поставить чеки на nil значение перечисления.

Что касается итерации, я не думаю, что когда-либо буду ее использовать, но я не вижу в этом вреда.

В качестве примера того, как это может быть объявлено:

type MetadataBlockType enum[uint] {
    StreamInfo:    0
    Padding:       1
    Application:   2
    SeekTable:     3
    VorbisComment: 4
    CueSheet:      5
    Picture:       6
}

Кроме того, стиль Swift для вывода типа и использования «точечного синтаксиса» был бы _хорошим_, но определенно не обязательным.

тип EnumA целое число
константа (
Неизвестный EnumA = йота
ААА
)


введите EnumB целое число
константа (
Неизвестный EnumB = йота
ВВВ
)

Два фрагмента кода не могут существовать ни в одном файле Go, ни в одном пакете, ни даже один из них может быть импортирован из другого пакета.

Пожалуйста, просто реализуйте способ C# реализации Enum:
type Days enum {Сб, Вс, Пн, Вт, Ср, Чт, Пт}
type Days enum[int] { Сб:1 , Вс, Вт, Ср, Чт, Пт}
type Days enum[string] { Сб: "Суббота", Вс: "Воскресенье" и т. д.}

@KamyarM Чем это лучше, чем

type Days int
const (
  Sat Days = 1+iota
  Sun
  ...
)

а также

type Days string
const (
  Sat Days = "Saturday"
  Sun      = "Sunday"
  ...
)

Прошу ограничить комментарии новыми подходами/аргументами. На эту ветку подписано много людей, и добавление шума/повторения может быть воспринято как неуважение к их времени и вниманию. Там много обсуждений ↑, включая подробные ответы на оба предыдущих комментария. Вы не согласитесь со всем сказанным, и ни одной из сторон может пока не понравиться итоги этого обсуждения, но простое его игнорирование также не поможет продвинуть его в продуктивном направлении.

Это лучше, потому что у него нет проблемы конфликта имен. Также поддерживает проверку типов компилятора. Упомянутый вами подход организовал его лучше, чем ничего, но компилятор не ограничивает вас в том, что вы можете ему назначить. Вы можете присвоить целое число, которое не является ни одним из дней, объекту этого типа:
вар дней
а =10
компилятор на самом деле ничего не делает по этому поводу. Так что в таком перечислении нет особого смысла. кроме того, что он лучше организован в IDE, таких как GoLand

Я хотел бы увидеть что-то подобное

type WeekDay enum string {
  Monday "mon"
  Tuesday "tue"
  // etc...
}

Или с автоматическим использованием iota :

// this assumes that iota automatically assigned to the first declared enum key
// and values have unsigned integer type
// value is positional, so if you decide to rename your key 
// you don't have to change everything in db
// also it is easy to grow your lists
type WeekDay enum {
  Monday
  Tuesday
}

Это обеспечит простоту и удобство в использовании:

func makeItWorkOn(day WeekDay) {
  // your implementation
}

Кроме того, enum должен иметь встроенный метод для проверки значения, чтобы мы могли проверить что-то из пользовательского ввода:

if day in WeekDay {
  makeItWorkOn(day)
}

И простые вещи, такие как:

if day == WeekDay.Monday {
 // whatever
}

Честно говоря, мой любимый синтаксис был бы таким (KISS):

// type automatically inferred from values or `iota`
enum WeekDay {
  Monday "mon"
  Tuesday "tue"
}

@zoonman Последний пример не следует следующему принципу Go: объявление функции начинается с func , объявление типа начинается с type , объявление переменной начинается с var , ...

@ md2perpe Я не пытаюсь следовать принципам «типа» Go, я пишу код каждый день, и единственный принцип, которому я следую, — не усложнять.
Чем больше кода вам нужно написать, чтобы следовать принципам, тем больше времени будет потрачено впустую.
TBH Я новичок в Go, но есть много вещей, которые я могу критиковать.
Например:

struct User {
  Id uint
  Email string
}

Легче написать и понять, чем

type User struct {
  Id uint
  Email string
}

Я могу привести пример, где должен использоваться тип:

// this is terrible because it blows your mind off
// especially if you Go newbie
// this should not be allowed by compiler/linter:
w := map[string]interface{}{"type": 0, "connected": true}

// it has to be splitted into type definition
type WeirdJSON map[string]interface{}

// and used like
w := WeirdJSON{"type": 0, "connected": true}

Раньше я писал код на Asm, C, C++, Pascal, Perl, PHP, Ruby, Python, JavaScript, TypeScript, теперь Go. Я все это видел. Этот опыт подсказывает мне, что код должен быть лаконичным, легко читаемым и понятным .

Я делаю проект машинного обучения, и мне нужно разобрать MIDI-файл.
Там мне нужно разобрать тайм-код SMPTE. Мне довольно сложно использовать идиоматический способ с йотой, но меня это не останавливает)

const (
        SMTPE0 int8 = ((-24 - (1 + (iota - 1) * 3) % 6 * (iota - 1) / ((iota - 1) | 0x01)) - 10 * ((iota - 1) % 2) - 5 * (iota / 3 - iota / 4) ) * iota / (iota | 0x01)
    SMTPE24 
    SMTPE25
    SMTPE29
    SMTPE30
)
const (
   _SMTPE0 int8 = 0 
   _SMTPE24 int8 = -24
   _SMTPE25 int8 = -25
   _SMTPE29 int8 = -29
   _SMTPE30 int8 = -30
)

Конечно, мне может понадобиться проверка во время выполнения с защитным программированием...

func IsSMTPE(status int8) bool {
    j := 4
    for i:= 0; i >= -30; i -= j % 6{
        if i == int(status){ 
            return true
        }
        j+=3
    }

    return status == 0
}

PlayGroundRef

Перечисления в некоторых случаях упрощают жизнь программистов. Enums — это всего лишь инструмент, и если вы используете его правильно, это может сэкономить время и повысить производительность. Я думаю, что нет проблем с реализацией этого в Go 2, как в C++, C# или других языках. Этот пример — всего лишь шутка, но он ясно показывает проблему.

@streeter12 streeter12 Я не понимаю, как ваш пример «четко показывает проблему». Как перечисления сделают этот код лучше или безопаснее?

Существует класс C# с реализацией той же логики перечисления.

 public enum SMTPE : sbyte
   {
        SMTPE0 = 0,
        SMTPE24 = -24,
        SMTPE25 = -25,
        SMTPE29 = -29,
        SMTPE30 = -30
   }

   public class TestClass
   {
        public readonly SMTPE smtpe;

        public TestClass(SMTPE smtpe)
        {
            this.smtpe = smtpe;
        }
   } 

С перечислениями времени компиляции я могу:

  1. Нет проверок во время выполнения.
  2. Значительно снижает вероятность ошибки команды (вы не можете передать неправильное значение во время компиляции).
  3. Это не противоречит концепции йоты.
  4. Легче понять логику, чем иметь одно имя для констант (важно, чтобы ваши константы представляли некоторые значения протокола низкого уровня).
  5. Вы можете сделать аналог метода ToString(), чтобы упростить представление значений. (CONNECTION_ERROR.NO_INTERNET лучше, чем 0x12). Я знаю о стрингере, но с перечислениями нет явной генерации кода.
  6. В некоторых языках вы можете получить массив значений, диапазон и т. д.
  7. Это просто понять при чтении кода (не нужно вычислять в голове).

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

@ streeter12 Спасибо, что разъяснили, что вы имели в виду. Единственное преимущество перед константами Go здесь заключается в том, что нельзя ввести недопустимое значение, потому что система типов не примет никакого другого значения, кроме одного из значений перечисления. Конечно, это приятно, но за это приходится платить: нет возможности расширить это перечисление за пределами этого кода. Внешнее расширение перечисления — одна из ключевых причин, по которой в Go мы отказались от стандартных перечислений.

Ответьте просто: нужно сделать так, чтобы некоторые расширения не использовали перечисления.
FE необходимо заставить конечный автомат использовать шаблон состояния вместо перечислений.

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

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

@ streeter12 К сожалению, реальность такова, что часто перечисления необходимо расширять.

@griesemer , расширяющий тип enum/sum, создает отдельный и иногда несовместимый тип.

Это по-прежнему верно в Go, даже несмотря на то, что для перечислений/сумм нет явных типов. Если у вас есть «тип перечисления» в пакете, который ожидает значения в {1, 2, 3}, и вы передаете ему 4 из вашего «расширенного типа перечисления», вы все равно нарушили контракт неявного «типа».

Если вам нужно расширить перечисление/сумму, вам также необходимо создать явные функции преобразования «В/От», которые явно обрабатывают иногда несовместимые случаи.

Я думаю, что разрыв между этим аргументом и людьми, поддерживающими это предложение или подобные предложения, такие как #19412, заключается в том, что мы считаем странным, что компромисс состоит в том, чтобы «всегда писать базовый код проверки, который может обработать компилятор», а не «иногда писать функции преобразования, которые вы вероятно, все равно придется писать».

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

Я думаю, что разрыв между этим аргументом и людьми, поддерживающими это предложение или подобные предложения, такие как #19412, заключается в том, что мы считаем странным, что компромисс состоит в том, чтобы «всегда писать базовый код проверки, который может обработать компилятор», а не «иногда писать функции преобразования, которые вы вероятно, все равно придется писать».

Очень хорошо сказано

@jimmyfrasche Лично я бы так не описал компромисс. Я бы сказал, что «всегда писать базовый код проверки, который может обработать компилятор», а не «добавлять совершенно новую концепцию в систему типов, которую должен изучить и понять каждый, кто использует Go».

Или, позвольте мне сказать это по-другому. Насколько я могу судить, единственные существенные особенности, отсутствующие в версии перечислимых типов Go, заключаются в том, что нет проверки присваивания нетипизированных констант, нет проверки явных преобразований и нет проверки того, что все значения были обработаны в выключатель. Мне кажется, что все эти функции не зависят от понятия перечисляемых типов. Тот факт, что в других языках есть перечисляемые типы, не должен приводить нас к выводу, что Go также нуждается в перечисляемых типах. Да, перечисляемые типы дадут нам эти недостающие функции. Но действительно ли необходимо добавлять совершенно новый тип, чтобы получить их? И стоит ли увеличение сложности языка преимуществ?

@ianlancetaylor Добавление сложности к языку, безусловно, является правильным вопросом, и «потому что это есть в другом языке», безусловно, не является аргументом. Лично я не думаю, что типы enum стоят того сами по себе. (Однако их обобщение, типы сумм, безусловно, ставят для меня много галочек).

Общий способ отказа типа от присваиваемости был бы хорош, хотя я не уверен, насколько широко это было бы полезно за пределами примитивов.

Я не уверен, насколько универсальной является концепция «проверить все значения, обрабатываемые в переключателе», без какого-либо способа сообщить компилятору полный список допустимых значений. Помимо типов enum и sum, единственное, о чем я могу думать, это что-то вроде типов диапазонов Ады, но они естественным образом не совместимы с нулевыми значениями, если только 0 не должен быть в диапазоне или сгенерирован код для обработки смещений всякий раз, когда они преобразуются или отражаются на. (В других языках были похожие семейства типов, некоторые в семействе паскалей, но в данный момент на ум приходит только Ада)

Во всяком случае, я имел в виду конкретно:

Единственное преимущество перед константами Go здесь заключается в том, что нельзя ввести недопустимое значение, потому что система типов не примет никакого другого значения, кроме одного из значений перечисления. Конечно, это приятно, но за это приходится платить: нет возможности расширить это перечисление за пределами этого кода. Внешнее расширение перечисления — одна из ключевых причин, по которой в Go мы отказались от стандартных перечислений.

а также

К сожалению, реальность такова, что часто перечисления необходимо расширять.

Этот аргумент не работает для меня по причинам, которые я изложил.

@jimmyfrasche Понятно; это трудная проблема. Вот почему в Go мы не пытались решить эту проблему, а вместо этого только предоставили механизм для простого создания последовательностей констант без необходимости повторения значения константы.

(Отправлено с задержкой - имелось в виду как ответ на https://github.com/golang/go/issues/19814#issuecomment-349158748)

@griesemer действительно, и это был определенно правильный призыв к Go 1, но некоторые из них стоит переоценить для Go 2.

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

Этот подход https://play.golang.org/p/7ud_3lrGfx дает вам все, кроме

  1. безопасность в определяющем пакете
  2. возможность линтить переключатель для полноты картины

Этот подход также можно использовать для небольших простых типов сумм †, но он более неудобен в использовании, поэтому я думаю, что что-то вроде https://github.com/golang/go/issues/19412#issuecomment -323208336 добавит к язык, и он может использоваться генератором кода для создания типов перечисления, которые позволяют избежать проблем 1 и 2.

† см. https://play.golang.org/p/YFffpsvx5e для эскиза json.Token с этой конструкцией

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

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

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

Конечно, вы можете возразить, что должна существовать граница доверия, где проверяется достоверность и реализуется требуемое поведение — и все внутри этой границы должно иметь возможность доверять этому поведению. И ментальная модель, кажется, такова, что эта граница доверия должна быть процессом. Потому что весь код в двоичном файле будет изменен атомарно и останется внутренне непротиворечивым. Но эта ментальная модель разрушается идеей постепенного восстановления; внезапно естественная граница доверия становится пакетом (или, может быть, репозиторием) в виде модулей, к которым вы применяете атомарное восстановление, и модуля, которому вы доверяете как самосогласованному.

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

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

@Merovius Процесс операционной системы и пакет, как вы выразились, являются границами доверия.

Информация, поступающая извне процесса, должна быть проверена на входе и преобразована в соответствующее представление для процесса, а в случае сбоя следует принять соответствующие меры. Это никогда не уходит. Я действительно не вижу там ничего конкретного для типов sum/enum. То же самое можно сказать и о структурах — иногда вы получаете лишние поля или слишком мало полей. Структуры по-прежнему полезны.

При этом с типом enum вы, конечно, можете включать случаи, специфичные для моделирования этих ошибок. Например

type FromTheNetwork enum {
  // pretend all the "valid" values are listed here
  Missing // the value was not included in the message
  Unknown // the value was not in the set of the valid values
  Error // there was an error attempting to read the value
}

и с типами суммы вы можете пойти дальше:

type FromTheNetwork pick {
  Missing struct{} // Not included in message
  Valid somepkg.TheSumBeingReceived // Include valid states with composition
  Unknown []byte // Raw bytes of unknown value received
  Error error // The error from attempting to read the value
}

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

На уровне пакета вам все еще нужно обрабатывать проверку высокого уровня, но проверка низкого уровня идет с типом. Я бы сказал, что сокращение домена этого типа помогает сохранить размер пакета в вашей голове. Это также делает намерение более понятным для инструментария, чтобы ваш редактор мог выписать все строки case X: и оставить вас для заполнения фактического кода, или можно было бы использовать линтер, чтобы убедиться, что весь код проверяет все случаи (вы отговорил меня от того, что ранее в компиляторе была исчерпывающая информация).

Я действительно не вижу там ничего конкретного для типов sum/enum. То же самое можно сказать и о структурах — иногда вы получаете лишние поля или слишком мало полей. Структуры по-прежнему полезны.

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

Аналогия со структурами объясняет это довольно точно: обещание совместимости Go 1 исключает литералы структуры без ключа из любого обещания - и, таким образом, использование литералов структуры с ключом было практикой, которая считалась настолько «лучшей», что у go vet есть проверка для этого. Причина точно такая же: если вы используете литералы структур без ключа, структуры больше не расширяемы.

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

При этом с типом enum вы, конечно, можете включать случаи, специфичные для моделирования этих ошибок.

Ваш пример охватывает только границу процесса (говоря о сетевых ошибках), а не границу пакета. Как поведут себя пакеты, если я добавлю "InvalidInternalState" (чтобы что-то придумать) к FromTheNetwork ? Должен ли я исправлять их переключатели, прежде чем они снова скомпилируются? Затем он не расширяется в модели постепенного ремонта. Требуют ли они в первую очередь для компиляции случай по умолчанию? Тогда, кажется, нет никакого смысла в перечислениях.

Опять же, наличие открытых перечислений - это другой вопрос. Я был бы согласен с такими вещами, как

Я бы сказал, что сокращение домена этого типа помогает сохранить размер пакета в вашей голове. Это также делает намерение более понятным для инструментов, чтобы ваш редактор мог выписать все строки case X: и оставить вас для заполнения фактического кода, или можно использовать линтер, чтобы убедиться, что весь код проверяет все случаи.

Но для этого нам не нужны настоящие перечисления как типы. Такой инструмент линтинга также может эвристически проверять объявления const с помощью iota , где каждый случай имеет заданный тип, и рассматривать это как «перечисление» и выполнять нужные проверки. Я был бы полностью согласен с инструментом, использующим эти «перечисления по соглашению», чтобы помочь автозаполнению или анализу, что каждый переключатель должен иметь значение по умолчанию или даже что каждый (известный) случай должен быть проверен. Я бы даже не возражал против добавления ключевого слова enum, которое в основном ведет себя так; то есть перечисление открыто (может принимать любое целочисленное значение), дает вам дополнительную область видимости и требует иметь значение по умолчанию в любом коммутаторе (я не думаю, что они добавят достаточно iota-enums для дополнительных затрат, но, по крайней мере, они не повредят моей повестке дня). Если это то, что предлагается - хорошо. Но, похоже, это не то, что имеет в виду большинство сторонников этого предложения (конечно, не исходного текста).

Мы можем не согласиться с важностью сохранения возможности постепенного исправления и расширения — например, многие люди считают, что семантическое управление версиями — лучшее решение проблем, которые оно решает. Но если вы находите их важными, вполне обоснованно и разумно рассматривать перечисления либо как вредные, либо как бессмысленные. И это был вопрос, на который я отвечал: как люди могут разумно пойти на компромисс, требуя проверки везде, вместо того, чтобы иметь ее в компиляторе. Ответ: ценя расширяемость и эволюцию API, что в любом случае делает эти проверки необходимыми на месте использования.

Время от времени противники enum говорили, что они не расширяемы, нам все еще нужны проверки после сериализации/перехода, мы можем восстановить совместимость и т.д.

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

Пример 1. Я низкоуровневый разработчик и мне нужен const для некоторых адресов регистров, установленных значений низкоуровневого протокола и т.д. На данный момент в Go у меня есть только одно решение: использовать const без iota, потому что во многих случаях это было бы некрасиво . Я могу получить несколько блоков констант для одного пакета и после нажатия . у меня есть все 20 констант, и если они имеют одинаковый тип и похожие имена, я могу ошибаться. Если проект большой, вы получите эту ошибку. Чтобы предотвратить это с помощью защитного программирования, TDD мы должны нарушать повторяющийся код проверки (дублирующийся код = повторяющиеся ошибки/тесты в любом случае). С использованием трансферов у нас не возникает подобных проблем и значения никогда не изменятся в этом случае (попробуйте найти ситуацию, когда адреса регистров будут меняться в продакшене:)). Мы по-прежнему иногда проверяем, находится ли значение, которое мы получаем из файла/сети и т. д., в диапазоне, но нет проблем сделать это централизованным (см. c# Enum.TryParseНапример). В этом случае с перечислениями я экономлю время разработки и производительность.

Пример 2. Я разрабатываю небольшой модуль с логикой состояний/ошибок. Если я сделаю перечисление частным, никто никогда не узнает об этом перечислении, и вы можете без проблем изменить/расширить его со всеми преимуществами 1. Если вы основывали свой код на какой-то частной логике, что-то пошло не так при вашей разработке.

Пример 3. Разрабатываю часто изменяемый и расширяемый модуль для широкого круга приложений. Было бы странным решением использовать перечисления или любые другие константы для определения общедоступной логики/интерфейса. Если вы добавите новый номер перечисления в клиент-серверную архитектуру, вы можете сломаться, но с константами вы можете получить непредсказуемое состояние модели и даже сохранить ее на диск. Я часто предпочитаю крах непредсказуемому состоянию. Это показывает нам, что проблема обратной совместимости/расширения является проблемой наших разработок, а не перечислений. Если вы понимаете, что перечисления не подходят в этом случае, просто не используйте их. Думаю, у нас достаточно компетенции, чтобы выбирать.

Основное различие между константами и перечислением времени компиляции, на мой взгляд, заключается в том, что перечисления имеют два основных контракта.

  1. Договор на имя.
  2. Контракт о ценностях.
    Все аргументы за и против этого пункта были рассмотрены ранее.
    Если вы используете контрактное программирование, вы легко поймете его преимущества.

Перечисляет, сколько других вещей имеют свои недостатки.
Fe его нельзя изменить без тормозной совместимости. Но если вы знаете O из принципов SOLID, это относится не только к enum, но и к разработке в целом. Кто-то может сказать, что я делаю свою программу с параллельной логикой и изменяемыми структурами уродливой. Запретим изменяемые структуры? Вместо этого мы можем добавить изменяемые/неизменяемые структуры и предоставить разработчикам право выбора.

После всего сказанного хочу отметить, что у Йоты есть и свои минусы.

  1. Он всегда имеет тип int,
  2. Вам нужно рассчитать значения в голове. Вы можете потерять много времени, пытаясь вычислить значения и проверить, что все в порядке.
    С enums/const я могу просто нажать F12 и просмотреть все значения.
  3. Йота-выражение — это кодовое выражение, которое вам также нужно протестировать.
    В некоторых проектах я полностью отказался от использования йоты по этим причинам.

Вы пытаетесь привести пример, где использование перечислений смешно.

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

И я даже не делал того, что вы говорите, то есть приводил пример того, где использование перечислений нелепо. Я взял пример, который должен был показать, насколько они необходимы, и проиллюстрировать, насколько они вредны.

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

Пример 1

Я мог бы дать вам «регистрационные имена» как что-то, что на самом деле не подлежит изменению, но что касается значений протокола, я непреклонен в том, что позиция, заключающаяся в том, чтобы они принимали произвольные значения для расширяемости и совместимости, является разумной. Опять же, proto2 -> proto3 содержал именно это изменение, и это было сделано на основе накопленного опыта.

И в любом случае, я не понимаю, почему линтер не сможет это поймать.

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

Если вы ошибаетесь в именах, закрытые перечисления вам не помогут. Только если вы не используете символические имена и вместо этого используете int/string-literals.

Пример 2

Лично я склонен ставить «один пакет» на строчку «небольшой проект». Таким образом, я считаю гораздо менее вероятным, что вы забудете регистр или измените место кода при расширении перечисления.

И в любом случае, я не понимаю, почему линтер не сможет это поймать.

Пример 3

Однако это наиболее распространенный вариант использования перечислений. Показательный пример: этот конкретный вопрос использует их в качестве оправдания. Другим случаем, который часто упоминается, являются системные вызовы — замаскированная архитектура клиент-сервер. Обобщение этого примера - «любой код, в котором два или более независимо разработанных компонента обмениваются такими значениями», что невероятно широко, охватывает подавляющее большинство вариантов их использования, а в рамках модели постепенного восстановления также любой экспортированный API .

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

Он всегда имеет тип int,

iota может (не обязательно, но что угодно), но блоки const не имеют, они могут иметь множество типов констант - фактически, надмножество наиболее часто предлагаемых реализаций перечисления.

Вам нужно рассчитать значения в голове.

Опять же, вы не можете использовать это как аргумент в пользу перечислений; вы можете записать константы так же, как и в объявлении перечисления.

Йота-выражение — это кодовое выражение, которое вам также нужно протестировать.

Не каждое выражение нужно тестировать. Если это сразу очевидно, тестирование излишне. Если это не так, запишите константы, вы все равно сделаете это в тесте.

iota не является рекомендуемым в настоящее время способом выполнения перечислений в Go — это объявления const . iota служит только более общим способом экономии ввода при написании последовательных или шаблонных объявлений констант.

И да, у открытых перечислений Go есть недостатки, очевидно. Они подробно упоминались выше: вы можете забыть случай в коммутаторе, что приведет к ошибкам. Они не используют пространство имен. Вы можете случайно использовать несимволическую константу, которая окажется недопустимым значением (что приведет к ошибкам).
Но мне кажется более продуктивным говорить об этих недостатках и сравнивать их с недостатками любого предложенного решения, чем брать фиксированное решение (тип перечисления) и спорить о его конкретных компромиссах для решения проблем.

На мой взгляд, большинство недостатков можно в основном решить прагматично в текущем языке, с помощью линтер-инструмента, обнаруживающего объявления const определенного типа и проверяющего их использование. Пространство имен не может быть решено таким образом, что не очень хорошо. Но может быть и другое решение этой проблемы, чем enums.

Я мог бы дать вам «регистрационные имена» как что-то, что на самом деле не подлежит изменению, но что касается значений протокола, я непреклонен в том, что позиция, заключающаяся в том, чтобы они принимали произвольные значения для расширяемости и совместимости, является разумной. Опять же, proto2 -> proto3 содержал именно это изменение, и это было сделано на основе накопленного опыта.

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

Если вы ошибаетесь в именах, закрытые перечисления вам не помогут. Только если вы не используете символические имена и вместо этого используете int/string-literals.

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

Однако это наиболее распространенный вариант использования перечислений. Показательный пример: этот конкретный вопрос использует их в качестве оправдания. Другим случаем, который часто упоминается, являются системные вызовы — замаскированная архитектура клиент-сервер. Обобщение этого примера - «любой код, в котором два или более независимо разработанных компонента обмениваются такими значениями», что невероятно широко, охватывает подавляющее большинство вариантов их использования, а в рамках модели постепенного восстановления также любой экспортированный API .

Но использование/не использование констант/перечислений не устраняет суть проблемы, вам все равно нужно подумать о обратной совместимости. Я хочу сказать, что проблема не в перечислениях/константах, а в наших вариантах использования.

Лично я склонен ставить «один пакет» на строчку «небольшой проект». Таким образом, я считаю гораздо менее вероятным, что вы забудете регистр или измените место кода при расширении перечисления.

В этом случае у вас все еще есть преимущества преобразования имени и проверки времени компиляции,

Не каждое выражение нужно тестировать. Если это сразу очевидно, тестирование излишне. Если это не так, запишите константы, вы все равно сделаете это в тесте.

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

Опять же, вы не можете использовать это как аргумент в пользу перечислений; вы можете записать константы так же, как и в объявлении перечисления.

Это не аргумент для перечислений.

@Меровиус

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

Они также не являются безопасно расширяемыми.

Если у тебя есть

package p
type Enum int
const (
  A Enum = iota
  B
  C
)
func Make() Enum {...}
func Take(Enum) {...}

а также

package q
import "p"
const D enum = p.C + 1

внутри q безопасно использовать D (если только следующая версия p не добавит собственный ярлык для Enum(3) ), но только до тех пор, пока вы никогда не передать его обратно в p : вы можете взять результат p.Make и перевести его состояние в D , но если вы вызываете p.Take , вы должны убедиться, что это не передается q.D И он должен гарантировать, что он получает только один из A , B , C или у вас есть ошибка. Вы можете обойти это, выполнив

package q
import "p"
type Enum int
const (
    A = p.A
    B = p.B
    C = p.C
    D = C + 1
)
// needs to return an error if passed D or an unknown state of Enum
func To(Enum) (p.Enum, error) {...}
// needs to return an error if p.Enum has a value not known to the author
// at the time this package was written.
func From(p.Enum) (Enum, error) {...}

С закрытым типом в языке или без него у вас есть все проблемы с закрытым типом, но без присмотра компилятора.

Ваш пример охватывает только границу процесса (говоря о сетевых ошибках), а не границу пакета. Как поведут себя пакеты, если я добавлю "InvalidInternalState" (чтобы что-то придумать) в FromTheNetwork? Должен ли я исправлять их переключатели, прежде чем они снова скомпилируются? Затем он не расширяется в модели постепенного ремонта. Требуют ли они в первую очередь для компиляции случай по умолчанию? Тогда, кажется, нет никакого смысла в перечислениях.

С помощью только типов enum вам все равно придется делать то же, что и выше, и определять свою собственную версию с дополнительным состоянием и писать функции преобразования.

Однако типы сумм компонуются даже при использовании в качестве перечислений, поэтому вы можете естественным и безопасным образом «расширить» один из них. Я привел пример, но чтобы быть более точным, учитывая

package p
type Enum pick {
  A, B, C struct{}
}

Enum можно "расширить" с помощью

package q
import "p"
type Enum pick {
  P p.Enum
  D struct{}
}

и на этот раз совершенно безопасно для новой версии p добавить D . Единственным недостатком является то, что вам нужно дважды переключиться, чтобы перейти в состояние p.Enum изнутри q.Enum , но это явно и ясно, и, как я уже упоминал, ваш редактор может выплюнуть скелет переключателей выключается автоматически.

Но для этого нам не нужны настоящие перечисления как типы. Такой инструмент linting может также эвристически проверять объявления const с помощью iota, где каждый случай имеет заданный тип, и рассматривать это как «перечисление» и выполнять нужные проверки. Я был бы полностью согласен с инструментом, использующим эти «перечисления по соглашению», чтобы помочь автозаполнению или анализу, что каждый переключатель должен иметь значение по умолчанию или даже что каждый (известный) случай должен быть проверен.

Есть две проблемы с этим.

Если у вас есть определенный интегральный тип с метками, присвоенными подмножеству его домена через const/iota:

Во-первых, он может представлять собой закрытое или открытое перечисление. Хотя в основном он используется для имитации закрытого типа, его также можно использовать просто для присвоения имен часто используемым значениям. Рассмотрим открытое перечисление для воображаемого формата файла:

const (
  //Name is the ID of a record field
  Name Record = iota
  //EmpID is the ID of an employee ID field
  EmpID

  //Intermediate values are reserved for future versions

  //Custom is the base of custom fields. Any custom field must have a unique ID greater than Custom.
  Custom Record = 42
)

Это не говорит о том, что 0, 1 и 42 являются доменом типа Record. Контракт гораздо тоньше и требует моделирования зависимых типов. (Это определенно зашло бы слишком далеко!)

Во-вторых, мы могли бы эвристически предположить, что определенный целочисленный тип с постоянными метками означает, что домен ограничен. Из приведенного выше можно получить ложное срабатывание, но нет ничего идеального. Мы могли бы использовать go/types для извлечения этого псевдотипа из определений, а затем просмотреть и найти все переключатели значений этого типа и убедиться, что все они содержат необходимые метки. Это может быть полезно, но пока мы не продемонстрировали исчерпывающую полноту. Мы обеспечили охват всех допустимых значений, но не доказали, что недействительные значения не были созданы. Это невозможно. Даже если бы мы могли найти каждый источник, приемник и преобразование значений и абстрактно интерпретировать их, чтобы статически гарантировать, что не было создано недопустимых значений, мы все равно не смогли бы ничего сказать о значении во время выполнения, поскольку отражение не знает об истинном значении. домен типа, так как он не закодирован в системе типов.

Здесь есть альтернатива типам enum и sum, которая обходит это, хотя и имеет свои проблемы.

Допустим, литерал типа range m n создает целочисленный тип, который имеет значение не менее m и не более n (для всех v m ≤ v ≤ n). При этом мы могли бы ограничить область перечисления, например

package p
type Enum range 0 2
const (
  A Enum = iota
  B
  C
)

Поскольку размер домена = количеству меток, можно проверить со 100% уверенностью, исчерпывает ли оператор switch все возможности. Чтобы расширить это перечисление извне, вам абсолютно необходимо создать функции преобразования типов для обработки отображения, но я по-прежнему утверждаю, что вам все равно нужно это сделать.

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

Мы можем не согласиться с важностью сохранения возможности постепенного исправления и расширения — например, многие люди считают, что семантическое управление версиями — лучшее решение проблем, которые оно решает. Но если вы находите их важными, вполне обоснованно и разумно рассматривать перечисления либо как вредные, либо как бессмысленные. И это был вопрос, на который я отвечал: как люди могут разумно пойти на компромисс, требуя проверки везде, вместо того, чтобы иметь ее в компиляторе. Ответ: ценя расширяемость и эволюцию API, что в любом случае делает эти проверки необходимыми на месте использования.

Для основных типов перечислений я согласен. В начале этого обсуждения я был бы просто недоволен, если бы они были выбраны вместо типов сумм, но теперь я понимаю, почему они были бы вредны. Спасибо вам и @griesemer за то, что разъяснили мне это.

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

Основная причина, по которой я использую golang вместо python, javascript и других распространенных бестиповых языков, — это безопасность типов. Я много работал с Java, и мне не хватает одной вещи в golang, которую предоставляет Java, — безопасных перечислений.

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

type enums enum { foo, bar, baz }

@rudolfschmidt , я с тобой согласен, это тоже может выглядеть так:

type DaysOfTheWeek enum {
  Monday
  Tuesday
}

Но у него есть небольшой подводный камень — мы должны иметь возможность взять под контроль enum в тех случаях, когда нам нужно проверить данные, преобразовать их в JSON или взаимодействовать с или FS.
Если мы слепо предположим, что enum представляет собой набор целых чисел без знака, мы можем получить йоту.
Если мы хотим внедрять инновации, мы должны думать об удобстве использования.
Например, как легко я могу проверить, что значение внутри входящего JSON является допустимым элементом перечисления?
Что, если я скажу вам, что программное обеспечение меняется?

Допустим, у нас есть список криптовалют:

type CryptoCurrency enum {
  BTC
  ETH
  XMR
}

Мы обмениваемся данными с несколькими сторонними системами. Допустим, у вас их тысячи.
У вас длинная история, количество данных в хранилище. Время идет, скажем так, Биткоин в конце концов умирает. Никто не использует его.
Итак, вы решаете удалить его из структуры:

type CryptoCurrency enum {
  ETH
  XMR
}

Это вызывает изменение данных. Потому что все значения enum сдвинуты. Это нормально для вас. Вы можете запустить миграцию для своих данных. Что касается ваших партнеров, некоторые из них не двигаются так быстро, некоторые не имеют ресурсов или просто не могут этого сделать по ряду причин.
Но у вас есть данные от них. Таким образом, у вас будет 2 перечисления: старый и новый; и сопоставитель данных, использующий оба.
Это говорит нам о гибкости определения перечислений и возможности проверки и упорядочения/деупорядочения таких данных.

type CryptoCurrency enum {
  ETH = 1, // reminds const?
  XMR = 2
}
// this is real life case 
v := 3
if v is CryptoCurrency {
 // right?
} else {
 // nope, provide default value
 v = CryptoCurrency.ETH
}

Мы должны думать о применимости перечислений и вариантов использования.

Мы можем выучить 2 новых ключевых слова, если это сэкономит нам тысячи строк шаблонного кода.

Золотая середина действительно заключается в том, чтобы иметь возможность проверять значения перечисления, не ограничивая их тем, какими могут быть эти значения. Тип Enum почти не изменился — это набор именованных констант. Переменная типа перечисления может быть равна любому значению базового типа перечисления. Вдобавок к этому вы добавляете возможность проверки значения, чтобы увидеть, содержит ли оно действительное значение перечисления или нет. И могут быть другие бонусы, такие как стрингификация.

Очень часто я оказываюсь в ситуации, когда у меня есть протокол (protobuf или thrift) с кучей перечислений повсюду. Я должен проверить каждое из них и, если значение перечисления мне неизвестно, отбросить это сообщение и сообщить об ошибке. У меня нет другого способа справиться с таким сообщением. В языках, где enum — это просто набор констант, у меня нет другого пути, кроме как писать огромное количество операторов switch, проверяющих все возможные комбинации. Это большой объем кода, и в нем обязательно будут ошибки. С чем-то вроде C# я могу использовать встроенную поддержку для проверки перечислений, что экономит много времени. Некоторые реализации protobuf на самом деле делают это внутри и выдают исключение, если это так. Не говоря уже о том, насколько простым становится ведение журнала — вы получаете строковое преобразование из коробки. Хорошо, что protobuf генерирует для вас реализацию Stringer, но не все в вашем коде является protobuf.

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

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

Мне кажется довольно простым предоставить эту функциональность в качестве инструмента. Часть его уже есть в стрингере-инструменте. Если есть инструмент, который вы бы назвали enumer Foo , который сгенерировал бы методы fmt.Stringer для Foo и (скажем) метод Known() bool , который проверяет, сохраненное значение находится в диапазоне известных значений, это облегчит ваши проблемы?

@Merovius в отсутствие чего-либо еще было бы полезно. Но я вообще против автогена. Единственный случай, когда это полезно и просто работает, - это такие вещи, как protobuf, когда у вас есть довольно стабильный протокол, который можно скомпилировать один раз. Использование его для перечислений в целом кажется костылем для упрощенной системы типов. Глядя на то, что Go относится к безопасности типов, это кажется противоречащим философии самого языка. Вместо того, чтобы помогать языку, вы начинаете разрабатывать эту инфраструктуру поверх него, которая на самом деле не является частью языковой экосистемы. Оставьте внешние инструменты для проверки, а не для реализации того, чего не хватает в языке.

. Использование его для перечислений в целом кажется костылем для упрощенной системы типов.

Потому что это - система типов Go, как известно, намеренно упрощена. Но вопрос был не в этом, вопрос был в том, облегчит ли это ваши проблемы. Помимо «мне это не нравится», я действительно не понимаю, как это не так (если вы все равно предполагаете открытые перечисления).

Глядя на то, что Go относится к безопасности типов, это кажется противоречащим философии самого языка.

Go не «все о безопасности типов». Такие языки, как Idris, ориентированы на безопасность типов. Go предназначен для крупномасштабных инженерных задач , и поэтому его дизайн определяется проблемами, которые он пытается решить. Например, его система типов позволяет выявлять широкий спектр ошибок из-за изменений API и позволяет выполнять некоторые крупномасштабные рефакторинги. Но он также намеренно сделан простым, чтобы облегчить обучение, уменьшить расхождение кодовых баз и повысить читабельность стороннего кода.

Таким образом, если интересующий вас вариант использования (открытые перечисления) может быть решен без изменения языка с помощью инструмента, который генерирует столь же легко читаемый код, то это очень соответствует философии Go. В частности, добавление новой языковой функции, являющейся подмножеством функциональности существующей, не соответствует дизайну Go.

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

Я объединил некоторые идеи из обсуждения, что вы об этом думаете?

Некоторые основные сведения:

  1. Вы можете расширить перечисление, как и любой другой тип.
  2. Они хранятся как константы, но с именем типа в качестве префикса. Причина: При использовании текущих iota-перечислений вы, вероятно, будете писать имя перечисления в качестве префикса каждой константы. С помощью этой функции вы можете просто избежать этого.
  3. Они неизменны и рассматриваются как любая другая константа.
  4. Вы можете перебирать перечисления. Когда вы это делаете, они ведут себя как карта. Ключ — это имя перечисления, значение — это значение перечисления.
  5. Вы можете добавлять методы к перечислению, как и к любому другому типу.
  6. Каждое значение перечисления должно автоматически генерироваться методами:
  7. Name() вернет имя переменной enum
  8. Index() вернет индекс перечисления, который автоматически увеличивается. Он начинается там, где начинается массив.

Код:
```иди
основной пакет

//Пример А
type Country enum[struct] { //перечисления могут расширять другие типы (см. пример B)
Austria("AT", "Austria", false) // Будет доступен как константа, но с типом as
Германия("DE", "Германия", true)//префикс (например, Страна.Австрия)

//The struct will automatically begin when it doesn't match the format EnumName(...) anymore
Code, CountryName string
HasMerkel bool //Totally awesome

}

//Перечисления могут иметь методы, как и любой другой тип
func (c Country) test() {}

основная функция () {
println(Country.Austria.CountryName) //Австрия
println(Страна.Германия.Код) //DE

/* Prints:
Austria
0
Germany
1
 */
for name, country := range Country {
    println(name) //Austria
    println(name == country.Name()) //true ; also autogenerated 
    println(country.Index()) //Auto generated increasing index
}

}

//Пример Б
тип Прохладность enum[int] {
ОченьКруто(10)
Круто(5)
Некруто(0)
}```

@sinnlosername Я думаю, что перечисления должны быть очень простыми для понимания. Объединение некоторых идей, представленных в предыдущем обсуждении, может не обязательно привести к лучшей идее для перечисления.

Я считаю, что следующее было бы просто понять:

Декларация

type Day enum {
    Monday
    Tuesday
    ...
    Sunday
}

Преобразование строк (с использованием интерфейса Stringer ):

func (d Day) String() string {
    switch d {
    case Monday:
        return "mon"
    case Tuesday:
        return "tues"
    ...
    case Sunday:
        return "sun"
    }
}

Это так просто. Преимущество этого заключается в обеспечении большей безопасности типов при передаче перечислений.

Пример использования

func IsWeekday(d Day) bool {
    return d != Saturday && d != Sunday
}

Если бы я использовал здесь константы string для представления Day , IsWeekday сказал бы, что любая строка, которая не является "sat" или "sun" является рабочим днем ​​(т. е. что должно вернуть IsWeekday("abc") ?). Напротив, домен функции, показанной выше, ограничен, что позволяет функции иметь больше смысла в отношении ее входных данных.

@ljeabmreosn

наверное должно быть

func IsWeekday(d Day) bool {
    return d != Day.Saturday && d != Day.Sunday
}

Я перестал ждать, пока команда golang улучшит язык необходимым образом. Я могу всем порекомендовать взглянуть на rust lang, в нем уже есть все необходимые функции, такие как enum, дженерики и многое другое.

Мы находимся в 14 мая 2018 года, и мы все еще обсуждаем поддержку enum. Я имею в виду, какого черта? Лично я разочарован от golang.

Я понимаю, что ожидание функции может разочаровать. Но такие неконструктивные комментарии не помогают. Пожалуйста, держите ваши комментарии уважительными. См . https://golang.org/conduct.

Спасибо.

@agnivade Я должен согласиться с @rudolfschmidt. GoLang определенно не является моим любимым языком из-за отсутствия функций, API и слишком большого сопротивления изменениям или принятия прошлых ошибок создателями Go. Но в данный момент у меня нет выбора, потому что я не принимал решения о том, какой язык выбрать для моего последнего проекта на моем рабочем месте. Так что приходится работать со всеми его недостатками. Но, если честно, это как пытка писать коды на GoLang ;-)

Я перестал ждать, пока команда golang улучшит язык необходимым образом.

  • Слово «надо» не означает «то, что я хочу».

На самом деле необходимы основные функции каждого современного языка. У GoLang есть несколько хороших функций, но он не выживет, если проект останется консервативным. Такие функции, как перечисления или дженерики, не имеют недостатков для тех, кому они не нравятся, но имеют много преимуществ для тех, кто хочет их использовать.

И не говорите мне, что «го хочет оставаться простым». Существует огромная разница между «простым» и «без реальных функций». Java очень проста, но в ней отсутствуют многие функции. Так что либо разработчики Java — волшебники, либо этот аргумент просто плохой.

На самом деле необходимы основные функции каждого современного языка.

Конечно. Эти основные функции называются полнотой по Тьюрингу. _Все_ остальное является выбором дизайна. Между полнотой по Тьюрингу и С++ (например) есть много пробелов, и в этом пространстве вы можете найти множество языков. Распределение предполагает, что глобального оптимума не существует.

У GoLang есть несколько хороших функций, но он не выживет, если проект останется консервативным.

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

Кстати, если я представлю, что будет с Go сегодня, если 10% функций, которые нужны людям, будут приняты, я бы к настоящему времени, вероятно, больше не использовал Go.

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

«У таких функций, как перечисления или дженерики, нет недостатков для людей, которым они не нравятся, но у них есть много преимуществ для людей, которые хотят их использовать».

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

Также java испытал своего рода «сбой», когда они пытались изменить важные вещи в java 9, что, вероятно, заставило многих людей искать альтернативу. Но посмотрите на java до этого краха. Он постоянно рос, потому что у него было все больше и больше функций, которые облегчали жизнь разработчикам.

«У таких функций, как перечисления или дженерики, нет недостатков для людей, которым они не нравятся, но у них есть много преимуществ для людей, которые хотят их использовать».

Это явно неправда. Каждая функция в конечном итоге попадет в stdlib и/или пакеты, которые я хочу импортировать. _Каждому_ придется иметь дело с новыми функциями, независимо от того, нравятся они им или нет.

Пока еще растет. ИМО, он бы не рос, если бы не был консервативным.

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

Если мы посмотрим на C# и Typescript или даже на Rust/Swift. Они добавляют новые функции как сумасшедшие. C# по-прежнему находится в топе языков, колеблясь вверх и вниз. Typescript растет очень быстро. То же самое для Rust/Swift. Популярность го, с другой стороны, резко возросла в 2009 и 2016 годах. Но между тем она вообще не росла, а фактически теряла. Go нечего дать новым разработчикам, если они уже знали о нем и по каким-то причинам не выбрали его раньше. Именно потому, что Go стагнирует в своем дизайне. Другие языки добавляют функцию не потому, что им больше нечего делать, а потому, что этого требует реальная пользовательская база. Людям нужны новые функции, чтобы их кодовая база оставалась актуальной в постоянно меняющихся проблемных областях. Как асинхронное/ожидание. Нужно было решить актуальную проблему. Неудивительно, что теперь вы можете увидеть его на многих языках.

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

Я не думаю, что его медленный рост (если он есть) связан не с консервативностью, а со стандартной библиотекой, уже существующим набором языковых функций, инструментарием. Вот что привело меня сюда.

И это результат консерватизма. Если язык ломает что-то/все каждые [пол]года или около того, вам не будет ничего из того, что вы говорите, что цените Go, потому что будет гораздо меньше людей, которые принесут вам это.

Добавление языковых функций ничего не изменит для меня в этом отношении.

Вы уверены, что? См. выше.


Кстати, вы видели результаты опроса 2017 года ?

Если язык ломает что-то/все каждые [пол]года или около того

Тогда ничего не сломай. C# добавил массу возможностей и никогда не нарушал обратную совместимость. Для них это тоже не вариант. То же самое для С++, я думаю. Если Go не может добавить функцию, не сломав что-то, то проблема в Go и, возможно, в том, как она реализована.

Кстати, вы видели результаты опроса 2017 года?

Мой комментарий основан на опросах 2017/2018, индексе TIOBE и моих общих наблюдениях за тем, что происходит с различными языками.

@cznic
Каждый должен иметь с ними дело, но вам не нужно их использовать. Если вы предпочитаете писать свой код с перечислениями и картами iota, вы все равно можете это сделать. И если вам не нравятся дженерики, то используйте библиотеки без них. Java доказывает, что возможно иметь и то, и другое.

Тогда ничего не сломай.

Хорошая идея. Однако многие, если не большинство предлагаемых изменений языка, _включая это самое_, являются критическими.

C# добавил массу возможностей и никогда не нарушал обратную совместимость.

Пожалуйста, проверьте свои факты: Visual C# 2010 Breaking Changes . (первый результат веб-поиска, я могу только догадываться, единственный ли это пример.)

Мой комментарий основан на опросах 2017/2018, индексе TIOBE и моих общих наблюдениях за тем, что происходит с различными языками.

Ну как же тогда можно видеть, что язык не растет, а результаты опроса показывают 70% рост числа респондентов из года в год?

Как вы определяете «критические изменения»? Каждая строка кода go по-прежнему будет работать после добавления перечислений или дженериков.

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

Я не могу согласиться. Как только язык получит, например, дженерики, то даже если я их не буду использовать, они будут использоваться повсюду. Даже если внутренне без изменения API. В результате они меня очень сильно задевают, потому что наверняка нет способа добавить дженерики к языку, не замедляя создание каких-либо программ, использующих их. Ака "Нет бесплатных обедов".

Как вы определяете «критические изменения»? Каждая строка кода go по-прежнему будет работать после добавления перечислений или дженериков.

Конечно, нет. Этот код больше не будет компилироваться с этим предложением:

package foo

var enum = 42

Слово «надо» не означает «то, что я хочу».

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

Golang претендует на звание языка для больших команд. Я не уверен, что вы можете использовать golang для разработки больших баз кода. Для этого вам нужна статическая компиляция и проверка типов, чтобы максимально избежать ошибок во время выполнения. Как вы можете сделать это без перечислений и дженериков? Эти функции даже не причудливы и не приятны, но абсолютно необходимы для серьезной разработки. Если у вас их нет, вы в конечном итоге будете использовать интерфейсы{} везде. Какой смысл иметь типы данных, если вы вынуждены использовать интерфейсы {} в своем коде?

Конечно, если у вас нет выбора, вы его тоже сделаете, но зачем вам, если у вас есть альтернативы, такие как rust, которые уже предлагают все это и работают даже быстрее, чем может быть golang? Мне действительно интересно, есть ли у Go будущее с таким мышлением:

Слово «надо» не означает «то, что я хочу».

Я уважаю любой вклад в открытый исходный код, и если golang — это проект-хобби, то это нормально, но к golang нужно относиться серьезно, и на данный момент это скорее игрушка для некоторых скучающих разработчиков, и я не вижу желания это изменить.

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

И то, и другое, немного более медленная компиляция и переменные, называемые «enum», являются минимальными эффектами. На самом деле 99% людей даже не заметят этого, а оставшемуся 1% просто нужно будет добавить небольшие изменения, которые терпимы. Это не сравнимо, например, с головоломкой Java, которая все портит.

И то, и другое, немного более медленная компиляция и переменные, называемые «enum», являются минимальными эффектами. На самом деле 99% людей даже не заметят этого, а оставшемуся 1% просто нужно будет добавить небольшие изменения, которые терпимы.

Все были бы счастливы, если бы кто-то мог предложить дизайн и реализацию с такой замечательной производительностью. Пожалуйста, внесите свой вклад в #15292.

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

Есть ли у вас какие-либо данные о разнице в скорости с дженериками?

И да, эти числа не подкреплены никакими данными, потому что они просто говорят о том, что вероятность наличия переменных с именем «enum» не очень высока.

Я хотел бы напомнить всем, что многие люди подписались на этот выпуск, чтобы задать конкретный вопрос о том, можно ли и как добавлять перечисления в Go. Общие вопросы "Является ли Go хорошим языком?" и «должен ли Go больше сосредоточиться на предоставлении функций?» наверное лучше обсудить на другом форуме.

Есть ли у вас какие-либо данные о разнице в скорости с дженериками?

Нет, именно поэтому я ничего не опубликовал. Я только написал, что стоимость не может быть нулевой.

И да, эти числа не подкреплены никакими данными, потому что они просто говорят о том, что вероятность наличия переменных с именем «enum» не очень высока.

Это перепутано. Замедление было связано с дженериками. «enum» было связано с обратной совместимостью, и ваша ложная «_Каждая_ строка кода go по-прежнему будет работать после добавления перечислений или дженериков». требовать. (подчеркивает мою)

@Merovius Ты прав, я затыкаюсь.

Возвращаясь к перечислимым типам, о которых идет речь в этом выпуске, я полностью понимаю аргумент о том, почему Go нужны дженерики, но я гораздо более шаток в аргументе о том, почему Go нужны перечисляемые типы. На самом деле, я спрашивал об этом выше в https://github.com/golang/go/issues/19814#issuecomment -290878151, и я все еще не уверен в этом. Если и был хороший ответ на этот вопрос, я его пропустил. Может ли кто-нибудь повторить или указать на это? Спасибо.

@ianlancetaylor Я не думаю, что вариант использования сложен, мне нужен безопасный способ гарантировать, что значение принадлежит предопределенному набору значений, что сегодня невозможно в Go. Единственный обходной путь — вручную проверять каждую возможную точку входа в ваш код, включая RPC и вызовы функций, что по своей сути ненадежно. Другие синтаксические тонкости итерации упрощают многие распространенные варианты использования. Считаете ли вы это ценным или нет, это субъективно, и, очевидно, ни один из приведенных выше аргументов не был убедительным для властей, поэтому я, по сути, отказался от того, чтобы это когда-либо рассматривалось на языковом уровне.

@ianlancetaylor : все связано с безопасностью типов. вы используете типы, чтобы свести к минимуму риск ошибок во время выполнения из-за опечатки или использования несовместимых типов. На данный момент вы можете написать в go

if enumReference == 1

потому что на данный момент перечисления — это просто числа или другие примитивные типы данных.

Этот код вообще невозможен, и его следует избегать. Та же самая дискуссия, которая была у вас в сообществе Java несколько лет назад, является причиной того, что они ввели перечисления, потому что понимали их важность.

Вы должны уметь писать только

if enumReference == enumType

не нужно слишком много фантазии, чтобы представить, в каких сценариях if enumReference == 1 может произойти более скрытым образом и привести к дополнительным проблемам, которые вы увидите только во время выполнения.

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

У Go есть свой потенциал, но странно, что вещи и концепции, которые проверены и понятны годами, обсуждаются здесь так же, как вы обсуждаете новые концепции или парадигмы программирования.

Afais, особенно после других дискуссий о дженериках, типах сумм и т. Д., Дело не столько в том, нужно ли это иметь, сколько в том, как это реализовать. Система типов Java чрезвычайно расширяема и хорошо определена. Это огромная разница.

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

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

@derekperkins @rudolfschmidt Спасибо. Я хочу прояснить, что, хотя в С++ есть типы перечислений, функции, которые вы предлагаете, отсутствуют в С++. Так что в этом нет ничего очевидного.,

В общем, если переменная типа перечисления может принимать значения только этого перечисления, это было бы бесполезно. В частности, должно быть преобразование из произвольного целого числа в тип enum; в противном случае вы не сможете отправлять перечисления по сетевому соединению. Ну, вы можете, но вам нужно написать переключатель с регистром для каждого значения перечисления, что кажется очень утомительным. Итак, при выполнении преобразования компилятор генерирует проверку того, что значение является допустимым значением перечисления во время преобразования типа? И паникует, если значение недействительно?

Должны ли значения перечисления быть последовательными или они могут принимать любое значение, как в C++?

В Go константы нетипизированы, поэтому, если мы разрешаем преобразования из целых чисел в тип перечисления, было бы странно запрещать if enumVal == 1 . Но я думаю, мы могли бы.

Один из общих принципов разработки Go заключается в том, что люди, пишущие Go, пишут код, а не типы. Я пока не вижу каких-либо преимуществ в перечислениях, которые помогают нам при написании кода. Кажется, они добавляют набор ограничений типа, которых у нас обычно нет в Go. Хорошо это или плохо, но Go не предоставляет механизмов для управления значениями типов. Так что я должен сказать, что мне аргумент в пользу добавления перечислений в Go пока не кажется убедительным.

Я повторюсь, но я за сохранение перечислений в том виде, в котором они есть сегодня, и добавление функций поверх них:

  • Тип перечисления имеет базовый тип значения и пару связанных с ним именованных констант.
  • компилятор должен разрешать преобразование произвольного значения в значение перечисления, если их базовые типы совместимы. Любое значение типа int должно быть преобразовано в любой целочисленный тип перечисления.
  • допускается преобразование, приводящее к недопустимому значению перечисления. Тип Enum не должен накладывать никаких ограничений на то, какие значения может принимать переменная.

Что он обеспечивает вдобавок к этому:

  • строковое перечисление значений. По моему опыту, очень полезно для пользовательского интерфейса и ведения журнала. Если значение перечисления допустимо, строковое преобразование возвращает имя константы. Если он недействителен, он возвращает строковое представление базового значения. Если -1 не является допустимым значением перечисления некоторого типа перечисления Foo , строковое преобразование должно просто вернуть -1 .
  • разрешить разработчику определить, является ли значение допустимым значением перечисления во время выполнения. Очень полезно при работе с любым типом протокола. По мере развития протоколов могут вводиться новые значения перечисления, о которых программа не знает. Или это может быть простая ошибка. Прямо сейчас вам нужно либо убедиться, что значения перечисления строго последовательны (не то, что вы всегда можете применить), либо вручную проверить каждое возможное значение. Такой код очень быстро становится очень большим, и ошибки неизбежно случаются.
  • возможно, позволить разработчику перечислить все возможные значения типа перечисления. Я видел, как люди просили об этом здесь, на другом языке это тоже есть, но я на самом деле не помню, чтобы мне это когда-нибудь понадобилось, поэтому у меня нет личного опыта в пользу этого.

Мое оправдание заключается в том, чтобы писать код и избегать ошибок. Все эти задачи утомительны и не нужны разработчику, чтобы выполнять их вручную или даже использовать внешние инструменты, усложняющие код и создание скриптов. Эти функции охватывают все, что мне нужно от перечислений, не слишком усложняя и ограничивая их. Я не думаю, что Go нужно что-то вроде перечислений в Swift или даже в Java.


Были дискуссии о проверке во время компиляции того, что оператор switch охватывает все возможные значения перечисления. С моим предложением это будет бесполезно. Выполнение проверок на исчерпание не будет охватывать недопустимые значения перечисления, поэтому вам все равно нужно иметь регистр по умолчанию для их обработки. Это необходимо для поддержки постепенного восстановления кода. Единственное, что мы можем здесь сделать, я думаю, это выдать предупреждение, если оператор switch не имеет регистра по умолчанию. Но это можно сделать даже без смены языка.

@ianlancetaylor Я думаю, что в вашем аргументе есть некоторые недостатки.

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

Абстракция для программиста — это хорошо; Go предоставляет множество абстракций. Например, следующий код не компилируется:

package main

import "fmt"

const NULL = 0x0

func main() {
    str := "hello"
    if &str == NULL {
        fmt.Println("str is null")
    }
}

но в C программа такого стиля будет компилироваться. Это связано с тем, что Go является строго типизированным, а C — нет.

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

@zerkms Да, это одна из возможностей, но, учитывая тип d , вывод типа должен быть возможен; однако квалифицированное использование перечислений (как в вашем примере) немного легче читать.

@ianlancetaylor , вы говорите о очень C-версии перечислений. Я уверен, что многие люди хотели бы этого, но, imo:

Значения Enum не должны иметь числовых свойств. Значения каждого типа перечисления должны быть их собственной конечной совокупностью дискретных меток, применяемых во время компиляции и не связанных с какими-либо числами или другими типами перечисления. Единственное, что вы можете сделать с парой таких значений, это == или != . Другие операции могут быть определены как методы или функции.

Реализация собирается скомпилировать эти значения в целые числа, но это не фундаментальная вещь с какой-либо законной причиной, которая должна быть представлена ​​​​непосредственно программисту, за исключением небезопасного или отражения. По той же причине, по которой вы не можете сделать bool(0) , чтобы получить false .

Если вы хотите преобразовать перечисление в или из числа или любого другого типа, вы записываете все случаи и включаете обработку ошибок, соответствующую ситуации. Если это утомительно, вы используете генератор кода, такой как стрингер, или, по крайней мере, что-то еще, чтобы заполнить случаи в операторе switch.

Если вы отправляете значение вне процесса, тип int хорош, если вы следуете четко определенному стандарту или знаете, что общаетесь с другим экземпляром вашей программы, который был скомпилирован из исходного кода, или вам нужно что-то сделать. помещается в минимально возможное пространство, даже если это может вызвать проблемы, но, как правило, ни один из них не выполняется, и лучше использовать строковое представление, чтобы на значение не влиял исходный порядок определения типа. Вы не хотите, чтобы зеленый процесс А стал синим процессом Б, потому что кто-то еще решил, что синий должен быть добавлен перед зеленым, чтобы сохранить в определении алфавитный порядок: вы хотите unrecognized color "Blue" .

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

(Конечно, часто вы хотите связать данные с этими состояниями, и тип этих данных варьируется от состояния к состоянию...)

@ljeabmreosn Моя точка зрения заключалась в том, что если Go разрешает преобразование из целых чисел в типы перечисления, то для нетипизированной константы было бы естественным автоматически преобразовываться в тип перечисления. Ваш контрпример отличается, поскольку Go не позволяет преобразовывать целые числа в типы указателей.

@jimmyfrasche Если вам нужно написать переключатель для преобразования между целыми числами и типами перечисления, то я согласен, что это будет работать чисто в Go, но, честно говоря, добавление к языку само по себе не кажется достаточно полезным. Это становится частным случаем типов сумм, о которых см. #19412.

Здесь много предложений.

Общий комментарий: для любого предложения, которое не предоставляет базовое значение (например, int), которое вы можете преобразовать в перечисление и из него, вот несколько вопросов, на которые нужно ответить.

Что такое нулевое значение перечислимого типа?

Как перейти от одного перечисления к другому? Я подозреваю, что для многих людей дни недели являются каноническим примером перечисления, но кто-то может разумно захотеть «увеличить» число со среды на четверг. Я бы не хотел писать для этого большой оператор switch.

(Кроме того, что касается «строковой обработки», правильная строка для дня недели зависит от языка и локали.)

@josharian stringification обычно означает автоматическое преобразование имен значений перечисления в строки компилятором. Без локализации и прочего. Если вы хотите создать что-то поверх этого, например, локализацию, то вы делаете это другими средствами, а другие языки предоставляют для этого богатый язык и инструменты фреймворка.

Например, некоторые типы C# имеют переопределение ToString , которое также принимает сведения о культуре. Или вы можете использовать сам объект DateTime и использовать его метод ToString , который принимает информацию как о формате, так и о культуре. Но эти переопределения не являются стандартными, класс object , от которого наследуется каждый, имеет только ToString() . Очень похоже на стрингерный интерфейс в Go.

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

@josharian Поскольку с точки зрения реализации это все равно будет int, а нулевые значения - это все биты, равные нулю, нулевое значение будет первым значением в исходном порядке. Это своего рода утечка int-ness, но на самом деле довольно приятно, потому что вы можете выбрать нулевое значение, решив, например, начинается ли неделя в понедельник или воскресенье. Конечно, менее приятно, что порядок остальных членов не имеет такого влияния и что изменение порядка значений может иметь нетривиальные последствия, если вы измените первый элемент. Однако на самом деле это ничем не отличается от const/iota.

Re stringification, что сказал @creker . Однако для расширения я ожидаю

var e enum {
  Sunday
  Monday
  //etc.
}
fmt.Println(reflect.ValueOf(e))

чтобы напечатать воскресенье, а не 0. Метка - это значение, а не его представление.

Чтобы быть ясным, я не говорю, что он должен иметь неявный метод String — просто метки должны храниться как часть типа и быть доступными посредством отражения. (Может быть, Println вызывает Label() для отражения. Значение из перечисления или что-то в этом роде? Не вникал глубоко в то, как fmt делает свое вуду.)

Как перейти от одного перечисления к другому? Я подозреваю, что для многих людей дни недели являются каноническим примером перечисления, но кто-то может разумно захотеть «увеличить» число со среды на четверг. Я бы не хотел писать для этого большой оператор switch.

Я думаю, что отражение или большой переключатель - это правильно. Общие шаблоны могут быть легко заполнены с помощью go generate для создания методов типа или фабричных функций этого типа (и, возможно, даже распознаются компилятором, чтобы снизить его до арифметического представления).

Мне не имеет смысла предполагать, что все перечисления имеют общий порядок или что они цикличны. Имея type failure enum { none; input; file; network } , действительно ли имеет смысл обеспечивать, чтобы неверный ввод был меньше, чем сбой файла, или что увеличение сбоя файла приводит к сбою сети, или что увеличение сбоя сети приводит к успеху?

Предполагая, что в основном используются циклические упорядоченные значения, другим способом решения этой проблемы может быть создание нового класса параметризованных целочисленных типов. Это плохой синтаксис, но для обсуждения предположим, что это I%N , где I — целочисленный тип, а N — целочисленная константа. Вся арифметика со значением этого типа неявно мод N. Тогда вы могли бы сделать

type Weekday uint%7
const (
  Sunday Weekday = iota
  //etc.

поэтому суббота + 1 == воскресенье и будний день (456) == понедельник. Невозможно построить недопустимый день недели. Однако это может быть полезно вне const/iota.

Когда вы вообще не хотите, чтобы это было числовое значение, как указал @ianlancetaylor , мне действительно нужны типы сумм.

Введение произвольного модулярного арифметического типа — интересное предложение. Тогда перечисления могут иметь такую ​​форму, которая дает вам тривиальный метод String:

var Weekdays = [...]string{"Sunday", ..., "Saturday"}

type Weekday = uint % len(Weekdays)

В сочетании с целыми числами произвольного размера это также дает вам int128, int256 и т. д.

Вы также можете определить некоторые встроенные функции:

type uint8 = uint%(1<<8)
// etc

Компилятор может доказать больше границ, чем раньше. И API могут предоставлять более точные утверждения через типы, например, функция Len64 в math/bits теперь может возвращать uint % 64 .

При работе над портом RISC-V мне нужен был тип uint12 , так как мои компоненты кодирования инструкций являются 12-битными; это могло быть uint % (1<<12) . Это может принести пользу многим битовым манипуляциям, особенно протоколам.

Минусы, конечно, существенные. Go имеет тенденцию отдавать предпочтение коду, а не типам, и это тяжело для типов. Такие операции, как + и -, могут внезапно стать такими же дорогими, как %. Без какой-либо параметризации типа вам, вероятно, придется преобразовать в канонические uint8 , uint16 и т. д., чтобы взаимодействовать практически с любой библиотечной функцией, а обратное преобразование может скрыть границы сбои (если только у нас нет способа выполнить преобразование паники при выходе за пределы диапазона, что вносит свою сложность). И я вижу, что им злоупотребляют, например, используя uint % 1000 для кодов состояния HTTP.

Тем не менее интересная идея. :)


Другие второстепенные ответы:

Это своего рода утечка информации

Это заставляет меня думать, что они действительно ints. :)

Общие шаблоны можно легко заполнить с помощью go generate

Если вам все равно нужно генерировать код с перечислениями, то мне кажется, что вы также можете генерировать функции String и проверки границ и т.п., И делать перечисления с генерацией кода вместо веса языковой поддержки.

Мне не имеет смысла предполагать, что все перечисления имеют общий порядок или что они цикличны.

Справедливо. Это заставляет меня думать, что наличие нескольких конкретных вариантов использования поможет внести ясность в то, что именно мы хотим от перечислений. Я подозреваю, что не будет четкого набора требований, и что эмуляция перечислений с использованием других языковых конструкций (то есть статус-кво) в конечном итоге будет иметь наибольший смысл. Но это только гипотеза.

Re stringification, что сказал @creker .

Справедливо. Но мне интересно, сколько дел в конечном итоге совпадают с днями недели. Что-нибудь, ориентированное на пользователя, конечно. И стробирование, по-видимому, является одним из основных требований к перечислениям.

@josharian Enums , которые действительно являются целыми числами, вероятно, нуждаются в аналогичном механизме. Иначе что такое enum { A; B; C}(42) ?

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

Это либо А, либо паника во время выполнения. В любом случае вы добавляете интегральный тип с ограниченным доменом. Если это паника во время выполнения, вы добавляете целочисленный тип, который паникует при переполнении, когда другие перекрываются. Если это A, вы добавили uint%N с некоторой церемонией.

Другой вариант — не допускать ни одного из A, B или C, но это то, что мы имеем сегодня с const/iota, так что выигрыша нет.

Все причины, по которым вы говорите, что int%N не войдет в язык, похоже, в равной степени применимы к перечислениям, которые являются своего рода целыми числами. (Хотя я бы никоим образом не рассердился, если бы что-то подобное было включено).

Устранение внутренней сущности устраняет эту загадку. Это требует генерации кода для случаев, когда вы хотите добавить часть этого int-ness, но также дает вам возможность не делать этого, что позволяет вам контролировать, сколько int-ness вводить и какого рода: вы можете добавить no " next", циклический метод next или метод next, возвращающий ошибку, если вы упадете с края. (Вы также не закончите такие вещи, как Monday*Sunday - Thursday ). Дополнительная жесткость делает его более податливым строительным материалом. Размеченное объединение хорошо моделирует разнообразие, отличное от int-y: pick { A, B, C struct{} } , среди прочего.

Основные преимущества наличия такой информации на языке заключаются в том, что

  1. Недопустимые значения являются незаконными.
  2. информация доступна для отражения и ввода / ввода, что позволяет программам воздействовать на нее без необходимости делать предположения или аннотации (которые в настоящее время недоступны для отражения).

Основные преимущества наличия такой информации в языке заключаются в следующем: Недопустимые значения недопустимы.

Я думаю, важно подчеркнуть, что не все видят в этом преимущество. Я, конечно, нет. Это часто облегчает потребление ценностей, но часто усложняет их создание. Что вы весите тяжелее, пока что зависит от личных предпочтений. Таким образом, возникает вопрос, является ли это чистой выгодой в целом.

Я также не вижу смысла запрещать недопустимые значения. Если у вас уже есть средства для самостоятельной проверки правильности (как в моем предложении выше), какую пользу дает это ограничение? Для меня это только усложняет ситуацию. В моих приложениях перечисления в большинстве случаев могли содержать недопустимые/неизвестные значения, и вам приходилось обходить это в зависимости от приложения — полностью выбросить, перейти на какое-то значение по умолчанию или сохранить как есть.

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

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

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

Я не понимаю этой логики. Типы — это наборы значений. Вы не можете присвоить тип переменной, значение которой не относится к этому типу. Я что-то неправильно понимаю?

PS: я согласен с тем, что перечисления являются частным случаем типов сумм, и этот вопрос должен иметь приоритет над этим.

Позвольте мне перефразировать/быть более точным: не все видят в закрытии перечислений преимущество.

Если вы хотите быть строгим в этом смысле, то а) «Недопустимые значения являются незаконными» — это тавтология и б) поэтому не может считаться преимуществом. С перечислениями, основанными на константах, в вашей интерпретации недопустимые значения также являются незаконными. Тип просто допускает гораздо больше значений.

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

В основном это просто const/iota, но вам не нужно запускать стрингер, поскольку пакет fmt может получать имена с помощью отражения. (Вам все равно придется запускать стрингер, если вы хотите, чтобы строки отличались от имен в источнике).

@jimmyfrasche stringification — это просто приятный бонус. Основная функция для меня, как вы можете прочитать в моем предложении выше, — это возможность проверить, является ли данное значение допустимым значением данного типа перечисления во время выполнения.

Например, учитывая что-то вроде этого

type Foo enum {
    Val1 = 1
    Val2 = 2
}

И метод отражения, например

func IsValidEnum(v {}interface) bool

Мы могли бы сделать что-то вроде этого

a := Foo.Val1
b := Foo(-1)
reflection.IsValidEnum(a) //returns true
reflection.IsValidEnum(b)  //returns false

В качестве примера из реального мира вы можете посмотреть на перечисления в C#, которые, на мой взгляд, идеально уловили эту золотую середину вместо того, чтобы слепо следовать тому, что сделала Java. Для проверки правильности в C# используется статический метод Enum.IsDefined .

@crecker Единственная разница между этим и const/iota заключается в
информация, хранящаяся в отражении. Это не так уж много для целого
новый тип типа.

Немного бредовая идея:

Храните имена и значения для всех констант, объявленных в одном пакете.
как их определенный тип таким образом, который может отразить. Это было бы
Странно, однако, выделять этот узкий класс использования const.

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

ИМО, это иллюстрирует одну из главных вещей, затягивающих это обсуждение: отсутствие ясности в отношении того, что такое набор «основных функций». Кажется, у всех немного разные представления об этом.
Лично мне по-прежнему нравится формат отчетов об опыте знакомства с этим набором. В списке даже есть один (хотя лично я все же отмечу тот факт, что в разделе «Что пошло не так» упоминается только то, что могло пойти не так, а не то, что произошло на самом деле ). Может быть, было бы полезно добавить пару, иллюстрирующих, где отсутствие проверки типов приводит к сбоям/ошибкам или, например, к неспособности выполнить крупномасштабный рефакторинг.

@jimmyfrasche , но это решает большую проблему во многих приложениях - проверку входных данных. Без какой-либо помощи со стороны системы типов вам придется делать это вручную, а это не то, что вы могли бы сделать за пару строк кода. Наличие некоторой формы проверки типа помогло бы решить эту проблему. Добавление к этому стрингификации упростило бы ведение журнала, поскольку у вас были бы правильно отформатированные имена, а не значения базового типа.

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

Просто посмотрите, что должен генерировать protobuf для Java, чтобы работать с перечислениями Java.

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

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

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

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

Я не уверен, что это идиоматический способ, и я также новичок в этом языке, но следующее работает и является кратким

type Day struct {
    value string
}

// optional, if you need string representation
func (d Day) String() string { return d.value }

var (
    Monday = Day{"Monday"}
    Tuesday = Day{"Tuesday"}
)

func main() {
    getTask(Monday)
}

func getTask(d Day) string {
    if d == Monday {
        fmt.Println("today is ", d, "!”) // today is Monday !
        return "running"
    }

    return "nothing to do"
}

Преимущества :

Недостатки :

Нам действительно нужны перечисления?

Что мешает кому-то сделать что-то вроде этого:

NotADay := Day{"NotADay"}
getTask(NotADay)

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

@bpkroth
Имея Day в собственном пакете и предоставляя только выбранные поля и методы, я не могу создавать новые значения типа Day за пределами package day
Кроме того, таким образом я не могу передавать анонимные структуры в getTask

./день/день.го

package day

type Day struct {
    value string
}

func (d Day) String() string { return d.value }

var (
    Monday  = Day{"Monday"}
    Tuesday = Day{"Tuesday"}
    Days    = []Day{Monday, Tuesday}
)

./main.go

package main

import (
    "fmt"
    "github.com/somePath/day"
)

func main() {
    january := day.Day{"january"} // implicit assignment of unexported field 'value' in day.Day literal

    var march struct {
        value string
    }
    march.value = "march"
    getTask(march) // cannot use march (type struct { value string }) as type day.Day in argument to getTask

    getTask(day.Monday)
}

func getTask(d day.Day) string {
    if d == day.Monday {
        fmt.Println("today is ", d, "!") // today is Monday !
        return "running"
    }

    return "nothing to do"
}

func iterateDays() {
    for _, d := range day.Days {
        fmt.Println(d)
    }
}

За всю свою жизнь я не видел ни одного другого языка, который настаивал бы на том, чтобы не добавлять самые простые и полезные функции, такие как перечисления, тернарные операторы, компиляция с неиспользуемыми переменными, типы сумм, дженерики, параметры по умолчанию и т. д.

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

@gh67uyyghj Кто-то пометил ваш комментарий как не по теме! и я думаю, что кто-то сделает то же самое на мой ответ. но я думаю, что ответ на ваш вопрос ДА. В GoLang быть безликим означает быть многофункциональным, поэтому все, чего нет в GoLang, на самом деле является функцией GoLang, которой нет в других языках программирования !!

@L-oris Это очень интересный способ реализации перечислений с типами. Но это кажется неудобным, и наличие ключевого слова enum (которое неизбежно еще больше усложняет язык) облегчило бы:

  • записывать
  • читать
  • причина о

В вашем примере (это здорово, потому что он работает сегодня) наличие перечислений (в той или иной форме) подразумевает необходимость:

  • Создать тип структуры
  • Создать метод
  • Создавать переменные (даже не константы, хотя пользователь библиотеки не может изменить эти значения)

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

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

Спасибо @andradei
Да, это обходной путь, но я чувствую, что цель языка - сделать его маленьким и простым.
Мы могли бы также возразить, что пропускаем уроки, но тогда давайте просто перейдем к Java :)

Я бы предпочел сосредоточиться на предложениях Go 2, например, на лучшей обработке ошибок. дал бы мне гораздо больше пользы, чем эти перечисления

Возвращаясь к вашим пунктам:

  • это не так много шаблонов; в худшем случае у нас могут быть какие-то генераторы (но неужели так много кода?)
  • насколько «простоты» мы добиваемся, добавляя новое ключевое слово, и весь определенный набор действий, которые оно, вероятно, будет иметь?
  • немного творчества с методами также может добавить интересные возможности этим перечислениям.
  • для удобочитаемости это больше о том, чтобы привыкнуть к нему; возможно, добавьте комментарий поверх него или префикс ваших переменных
package day

// Day Enum
type Day struct {
    value string
}

@L-oris Я вижу. Я также в восторге от предложений Go 2. Я бы сказал, что дженерики усложнят язык больше, чем перечисления. Но придерживаться ваших пунктов:

  • Это не так уж и шаблонно на самом деле
  • Нам нужно проверить, насколько широко известна концепция enum, чтобы быть уверенным, я бы сказал, что большинство людей знают, что это такое (но я не могу этого доказать). Сложность языка была бы хорошей «ценой», чтобы заплатить за его преимущества.
  • Это правда, отсутствие перечислений — это проблемы, с которыми я столкнулся только при проверке кода Generetad Protobuf и при попытке создать модель базы данных, которая имитирует перечисление, например.
  • Это также верно.

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

Ребята, очень хочу эту функцию на будущее!. Указатели и способ определения " перечислений " в _nowadays_ не очень хорошо ладят. Например: https://play.golang.org/p/A7rjgAMjfCx

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

package application

type Status struct {
Name string
isFinal bool
}

enum Status {
     Started = &Status{"Started",false}
     Stopped = &Status{"Stopped",true}
     Canceled = &Status{"Canceled",true}
}

// application.Status.Start - to use

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

Для этого Go должен сначала поддерживать глубокие неизменяемые структуры. Я могу представить, что без неизменяемых типов вы могли бы сделать это с перечислениями, чтобы получить то же самое:

type Status enum {
  Started
  Stopped
}

func isFinal(s Status) bool {
  exhaustive switch(s) {
    case Started: return false;
    case Stopped: return true;
  }
}

мне кажется должно быть проще

func isFinal(s Status) bool {
  return s == Status.Stopped
}

Предложение для Go2

Логически перечисления должны предоставлять интерфейс типов.
Ранее я говорил, что перечисления должны быть отдельными.
Это явно названные константы, привязанные к определенному пространству имен.

enum Status uint8 {
  Started  // Status.Started == 0
  Stopped // Status.Stopped == 1, etc, like we have used iota
}
// or 
enum Status string  {
  Started // Status.Started == "Started", like it works with JSON
  Stopped // Status.Stopped == "Stopped", etc
}
// unless you wanna define its values explicitly
enum Status {
  Started "started"  // compiler can infer underlying type
  Stopped "finished"
}
// and enums are type extensions and should be used like this
type MyStatus Status

MyStatus validatedStatus // holds a nil until initialized

// for status value validation we can use map pattern
if validatedStatus, ok := MyStatus[s]; ok {
  // this value is a valid status
  // and we can use it later as regular read-only string
  // or like this
  if validatedStatus == MyStatus.Started {
     fmt.Printf("Hey, my status is %s", validatedStatus)
  }
}

Перечисления — это расширения типов, «контейнеры констант».

Для любителей шрифтов

Варианты синтаксиса для тех, кто хочет видеть его как тип

type Status uint8 enum {
  Started  // Status.Started == 0
  Stopped // Status.Stopped == 1, etc, like we have used iota
}

Но мы также можем избежать этих явных объявлений верхнего уровня.

type Status enum {
  Started  // Status.Started == 0
  Stopped // Status.Stopped == 1, etc, like we have used iota
}

Пример проверки остается прежним.

но на всякий случай

type Status1 uint8 enum {
  Started  // Status1.Started == 0
  Stopped // Status1.Stopped == 1, etc, like we have used iota
}

type Status2 uint8 enum {
  Started  // Status1.Started == 0
  Stopped // Status1.Stopped == 1, etc, like we have used iota
}

Как насчет Status1.Started == Status2.Started?
о маршалинге?

Если я сменю позицию?

type Status uint8 enum {
  Started  // Status.Started == 0
  InProcess
  Stopped // Status.Stopped == 1, etc, like we have used iota
}

Я согласен с @Goodwine по поводу неизменяемых типов.

Маршалинг — интересный вопрос.
Все зависит от того, как мы будем обращаться с базовым значением. Если мы собираемся использовать фактические значения, то Status1.Started будет равно Status2.Started .
Если мы будем использовать символическую интерпретацию, это будет рассматриваться как разные значения.

Вставка чего-либо приведет к изменению значений (точно так же, как это происходит с iota ).
Чтобы избежать этого, разработчик должен указывать значения вместе с объявлениями.

type Status uint8 enum {
  Started  0
  InProcess 2
  Stopped 1
}

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

Мне нравится, как в Rust реализованы Enums.

По умолчанию без указания типа

enum IpAddr {
    V4,
    V6,
}

Пользовательский тип

enum IpAddr {
    V4(string),
    V6(string),
}

home := IpAddr.V4("127.0.0.1");
loopback := IpAddr.V6("::1");

Сложные типы

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

Наверняка даже иметь простые перечисления, как в C#, которые хранятся как целочисленные типы, было бы здорово.

Вышеупомянутое выходит за рамки enum s, это _различаемые союзы_, которые действительно более мощные, особенно с _сопоставлением шаблонов_, которое может быть незначительным расширением switch , что-то вроде:

switch something.(type) {
case Quit:
        ...
case ChangeColor; r, g, b := something:
        ...
case Write: // Here `something` is known to be a string
        ...
// Ideally Go would warn here about the missing case for "Move"
}

Мне не нужны никакие проверки перечислений во время компиляции, так как это может быть опасно, как уже упоминалось.

Мне нужно было несколько раз перебрать все константы данного типа:

  • либо для проверки (если мы очень уверены, что хотим принять это, либо просто игнорировать неизвестные параметры)

    • или для списка возможных констант (подумайте о выпадающих списках).

Мы могли бы сделать проверку с помощью iota и указать конец списка. Однако использование iota для чего-то еще, кроме как внутри кода, было бы довольно опасно, потому что что-то сломается, если вставить константу в неправильную строку (я знаю, что нам нужно знать, где мы размещаем вещи в программировании, но подобная ошибка гораздо труднее найти, чем другие вещи). Кроме того, у нас нет описания того, что на самом деле означает константа, когда она является числом. Это приводит к следующему пункту:

Хорошим дополнением было бы указать для него строковые имена.

Что мешает кому-то сделать что-то вроде этого:

NotADay := Day{"NotADay"}
getTask(NotADay)

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

@ L-oris Так что насчет этого:

package main
import "yet/it/is/not/a/good/practice/in/Go/enum/example/day"

func main()
{
  // var foo day.Day
  foo := day.Day{}
  bar(foo)
}

func bar(day day.Day)
{
  // xxxxxxxxxx
}

То, что мы хотим, это НЕ ЗАМЕЧАНИЕ ВО ВРЕМЯ РАБОТЫ и Странная ОШИБКА, вызванная [возврат «нечего делать»], а ОТЧЕТ О ОБ ОШИБКАХ во время компиляции / времени кодирования !
ПОНИМАТЬ?

  1. enum действительно является новым типом, что и делает type State string , нет идиоматической необходимости вводить новое ключевое слово. Цель Go не в экономии места в исходном коде, а в удобочитаемости и ясности цели.

  2. Ключевым препятствием является отсутствие безопасности типов, путаница новых типов на основе string или int с фактическими строками/целыми числами. Все предложения enum объявлены как const , что создает набор известных значений, которые компилятор может проверить.

  3. Интерфейс Stringer — это идиома для представления любого типа в виде удобочитаемого текста. Без настройки перечисления type ContextKey string это строковое значение, а для сгенерированных iota перечислений это целое число, очень похожее на коды XHR ReadyState (0 - не отправлено, 4 - выполнено) в JavaScript.

    Скорее, проблема заключается в ошибочности пользовательской реализации func (k ContextKey) String() string , которая обычно выполняется с использованием переключателя, который должен содержать каждую известную константу предложения перечисления.

  4. В таком языке, как Swift, есть понятие исчерпывающего переключения. Это хороший подход как для проверки типов по набору const s, так и для создания идиоматического способа вызова этой проверки. Функция String() , будучи обычной необходимостью, является отличным примером для реализации.

Предложение

package main

import (
    "context"
    "strconv"
    "fmt"
    "os"
)

// State is an enum of known system states.
type DeepThoughtState int

// One of known system states.
const (
    Unknown DeepThoughtState = iota
    Init
    Working
    Paused
    ShutDown
)

// String returns a human-readable description of the State.
//
// It switches over const State values and if called on
// variable of type State it will fall through to a default
// system representation of State as a string (string of integer
// will be just digits).
func (s DeepThoughtState) String() string {
    // NEW: Switch only over const values for State
    switch s.(const) {
    case Unknown:
        return fmt.Printf("%d - the state of the system is not yet known", Unknown)
    case Init:
        return fmt.Printf("%d - the system is initializing", Init)
    } // ERR: const switch must be exhaustive; add all cases or `default` clause

    // ERR: no return at the end of the function (switch is not exhaustive)
}

// RegisterState allows changing the state
func RegisterState(ctx context.Context, state string) (interface{}, error) {
    next, err := strconv.ParseInt(state, 10, 32)
    if err != nil {
        return nil, err
    }
    nextState := DeepThoughtState(next)

    fmt.Printf("RegisterState=%s\n", nextState) // naive logging

        // NEW: Check dynamically if variable is a known constant
    if st, ok := nextState.(const); ok {
        // TODO: Persist new state
        return st, nil
    } else {
        return nil, fmt.Errorf("unknown state %d, new state must be one of known integers", nextState)
    }
}

func main() {
    _, err := RegisterState(context.Background(), "42")
    if err != nil {
        fmt.Println("error", err)
        os.Exit(1)
    }
    os.Exit(0)
    return
}

PS Связанные значения в перечислениях Swift — один из моих любимых трюков. В Go для них нет места. Если вы хотите, чтобы рядом с вашими данными перечисления было значение — используйте строго типизированную struct , обертывающую их.

Несколько месяцев назад я написал доказательство концепции линтера, который проверяет, правильно ли обрабатываются перечисляемые типы. https://github.com/loov/enumcheck

В настоящее время он использует комментарии, чтобы пометить вещи как перечисления:

type Letter byte // enumcheck

const (
    Alpha Letter = iota
    Beta
    Gamma
)

func Switch(x Letter) {
    switch x { // error: "missing cases Beta and Gamma"
    case Alpha:
        fmt.Println("alpha")
    case 4: // error: "implicit conversion of 4 to Letter"
        fmt.Println("beta")
    default: // error: "Letter shouldn't have a default case"
        fmt.Println("default")
    }
}

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

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

Текущая реализация перечислений в Go1 — самая странная и неочевидная реализация перечислений на любом известном мне языке. Даже C реализует их лучше. Эта штука с йотой выглядит как взлом. И вообще, что значит йота? Как я должен запомнить это ключевое слово? Го должен быть легким в освоении. Но это просто qiurky.

@пофл :
Хотя я согласен с тем, что перечисления в Go довольно неуклюжи, iota на самом деле просто обычное английское слово:

йота
_имя существительное_

  1. очень небольшое количество; йота; белый
  2. девятая буква греческого алфавита (I, ι).
  3. гласный звук, обозначаемый этой буквой.

Предположительно, они шли к определению номер один с точки зрения использования в языке.

На заметку в ответ на более старый комментарий здесь:
Хотя мне бы очень хотелось, чтобы в Go были разделенные союзы, я чувствую, что они должны быть отделены от настоящих перечислений. С тем, как в настоящее время работают дженерики, вы можете получить что-то очень похожее на размеченные объединения через списки типов в интерфейсах. См. № 41716.

Использование iota в Go во многом основано на его использовании в APL. Цитирую https://en.wikipedia.org/wiki/Iota :

В некоторых языках программирования (например, A+, APL, C++[6], Go[7]) iota (либо строчный символ ⍳, либо идентификатор iota) используется для представления и генерации массива последовательных целых чисел. Например, в APL ⍳4 дает 1 2 3 4.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги