Go: proposta: especificação: adicionar suporte a enum digitado

Criado em 1 abr. 2017  ·  180Comentários  ·  Fonte: golang/go

Eu gostaria de propor que enum seja adicionado ao Go como um tipo especial de type . Os exemplos abaixo são emprestados do exemplo protobuf.

Enums em Go hoje

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
}

Como pode ficar com o suporte a idiomas

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"
}

O padrão é comum o suficiente para que eu ache que justifique o uso de maiúsculas e minúsculas e acredito que torna o código mais legível. Na camada de implementação, imagino que a maioria dos casos pode ser verificada em tempo de compilação, alguns dos quais já acontecem hoje, enquanto outros são quase impossíveis ou exigem compensações significativas.

  • Segurança para tipos exportados : nada impede alguém de fazer SearchRequest(99) ou SearchRequest("MOBILEAPP") . As soluções alternativas atuais incluem fazer um tipo não exportado com opções, mas isso geralmente torna o código resultante mais difícil de usar/documentar.
  • Segurança em tempo de execução : Assim como o protobuf verifica a validade durante o desempacotamento, isso fornece validação em toda a linguagem, sempre que um enum é instanciado.
  • Ferramentas / Documentação : muitos pacotes hoje colocam opções válidas nos comentários de campo, mas nem todos fazem isso e não há garantia de que os comentários não estejam desatualizados.

Coisas a considerar

  • Nil : implementando enum no topo do sistema de tipos, não acredito que isso deva exigir uma caixa especial. Se alguém quiser que nil seja válido, então o enum deve ser definido como um ponteiro.
  • Valor padrão / atribuições de tempo de execução : Esta é uma das decisões mais difíceis de tomar. E se o valor padrão Go não estiver definido como uma enumeração válida? A análise estática pode mitigar parte disso em tempo de compilação, mas precisaria haver uma maneira de lidar com entradas externas.

Eu não tenho nenhuma opinião forte sobre a sintaxe. Eu acredito que isso poderia ser bem feito e teria um impacto positivo no ecossistema.

Go2 LanguageChange NeedsInvestigation Proposal

Comentários muito úteis

@md2perpe que não é enums.

  1. Eles não podem ser enumerados, iterados.
  2. Eles não têm representação de string útil.
  3. Eles não têm identidade:

```vai
pacote principal

importar (
"fm"
)

func main(){
digite SearchRequest int
const (
Solicitação de pesquisa universal = iota
Rede
)

const (
    Another SearchRequest = iota
    Foo
)

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

}
````

Eu concordo totalmente com @derekperkins que Go precisa de algum enum como cidadão de primeira classe. Como isso ficaria, não tenho certeza, mas suspeito que poderia ser feito sem quebrar a casa de vidro do Go 1.

Todos 180 comentários

@derekparker há uma discussão para fazer uma proposta Go2 em #19412

Eu li isso hoje cedo, mas parecia mais focado em tipos válidos, onde isso é focado em valores de tipo válidos. Talvez este seja um subconjunto dessa proposta, mas também é uma mudança menos abrangente no sistema de tipos que poderia ser colocado em Go hoje.

enums são um caso especial de tipos de soma onde todos os tipos são iguais e há um valor associado a cada um por um método. Mais para digitar, com certeza, mas mesmo efeito. Independentemente disso, seria um ou outro, os tipos de soma cobrem mais terreno e até os tipos de soma são improváveis. Nada está acontecendo até Go2 por causa do acordo de compatibilidade Go1, de qualquer forma, já que essas propostas exigiriam, no mínimo, uma nova palavra-chave, caso alguma delas fosse aceita

É justo, mas nenhuma dessas propostas está quebrando o acordo de compatibilidade. Houve uma opinião expressa de que os tipos de soma eram "muito grandes" para serem adicionados ao Go1. Se for esse o caso, então esta proposta é um meio-termo valioso que pode ser um trampolim para os tipos de soma total em Go2.

Ambos exigem uma nova palavra-chave que quebraria o código Go1 válido usando isso como um identificador

acho que isso poderia ser trabalhado

Um novo recurso de linguagem precisa de casos de uso convincentes. Todos os recursos da linguagem são úteis, ou ninguém os proporia; a questão é: eles são úteis o suficiente para justificar complicar a linguagem e exigir que todos aprendam os novos conceitos? Quais são os casos de uso atraentes aqui? Como as pessoas vão usar isso? Por exemplo, as pessoas esperariam poder iterar sobre o conjunto de valores de enumeração válidos e, em caso afirmativo, como eles fariam isso? Esta proposta faz mais do que permitir que você evite adicionar casos padrão a alguns switches?

Aqui está a maneira idiomática de escrever enumerações no Go atual:

type SearchRequest int

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

Isso tem a vantagem de ser fácil criar sinalizadores que podem ser OR:ed (usando o operador | ):

type SearchRequest int

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

Não consigo ver que a introdução de uma palavra-chave enum a tornaria muito mais curta.

@md2perpe que não é enums.

  1. Eles não podem ser enumerados, iterados.
  2. Eles não têm representação de string útil.
  3. Eles não têm identidade:

```vai
pacote principal

importar (
"fm"
)

func main(){
digite SearchRequest int
const (
Solicitação de pesquisa universal = iota
Rede
)

const (
    Another SearchRequest = iota
    Foo
)

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

}
````

Eu concordo totalmente com @derekperkins que Go precisa de algum enum como cidadão de primeira classe. Como isso ficaria, não tenho certeza, mas suspeito que poderia ser feito sem quebrar a casa de vidro do Go 1.

@md2perpe iota é uma maneira muito limitada de abordar enums, o que funciona muito bem para um conjunto limitado de circunstâncias.

  1. Você precisa de um int
  2. Você só precisa ser consistente dentro do seu pacote, não representando o estado externo

Assim que você precisa representar uma string ou outro tipo, o que é muito comum para sinalizadores externos, iota não funciona para você. Se você deseja corresponder a uma representação externa/do banco de dados, eu não usaria iota , porque a ordenação no código-fonte é importante e a reordenação causaria problemas de integridade de dados.

Este não é apenas um problema de conveniência para tornar o código mais curto. Esta é uma proposta que permitirá a integridade dos dados de uma forma que não é aplicável pela linguagem atual.

@ianlancetaylor

Por exemplo, as pessoas esperariam poder iterar sobre o conjunto de valores de enumeração válidos e, em caso afirmativo, como eles fariam isso?

Acho que é um caso de uso sólido, conforme mencionado por @bep. Acho que a iteração se pareceria com um loop Go padrão e acho que eles fariam um loop na ordem em que foram definidos.

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

Se Go fosse adicionar algo mais do que iota, nesse ponto, por que não usar tipos de dados algébricos?

Por extensão de ordenação de acordo com a ordem de definição, e seguindo o exemplo do protobuf, acho que o valor padrão do campo seria o primeiro campo definido.

@bep Não é tão conveniente, mas você pode obter todas essas propriedades:

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)
    }
}

Eu não acho que enums verificados em tempo de compilação sejam uma boa ideia. Eu acredito que go praticamente tem isso agora . Meu raciocínio é

  • enums verificados em tempo de compilação não são compatíveis com versões anteriores nem com versões anteriores para o caso de adições ou remoções. #18130 gasta um esforço significativo para avançar no sentido de permitir o reparo gradual do código; enums destruiriam esse esforço; qualquer pacote que desejasse alterar um conjunto de enums quebraria automática e forçosamente todos os seus importadores.
  • Ao contrário do que o comentário original afirma, o protobuf (por esse motivo específico) não verifica a validade dos campos enum. proto2 especifica que um valor desconhecido para um enum deve ser tratado como um campo desconhecido e proto3 ainda especifica que o código gerado deve ter uma maneira de representá-los com o valor codificado (exatamente como go faz atualmente com fake-enums)
  • No final, não acrescenta muito. Você pode obter stringificação usando a ferramenta stringer. Você pode obter iteração, adicionando um const MaxValidFoo sentinela (mas veja a advertência acima. Você nem deveria ter o requisito). Você simplesmente não deveria ter os dois const-decls em primeiro lugar. Basta integrar uma ferramenta em seu CI que verifica isso.
  • Eu não acredito que outros tipos além de ints sejam realmente necessários. A ferramenta de stringer já deve cobrir a conversão de e para strings; no final, o código gerado seria equivalente ao que um compilador geraria de qualquer maneira (a menos que você sugira seriamente que qualquer comparação em "string-enums" iteraria os bytes ...)

No geral, apenas um enorme -1 para mim. Não só não acrescenta nada; dói ativamente.

Acho que a implementação atual de enum em Go é muito direta e fornece verificações de tempo de compilação suficientes. Na verdade, espero algum tipo de enums Rust com correspondência básica de padrões, mas possivelmente quebra as garantias do Go1.

Como enums são um caso especial de tipos de soma e a sabedoria comum é que devemos usar interfaces para simular tipos de soma, a resposta é claramente https://play.golang.org/p/1BvOakvbj2

(se não estiver claro: sim, isso é uma piada – no estilo clássico de programador, estou errado).

Com toda a seriedade, para os recursos discutidos neste tópico, algumas ferramentas extras seriam úteis.

Como a ferramenta stringer, uma ferramenta "ranger" pode gerar o equivalente à função Iter no código que vinculei acima.

Algo poderia gerar implementações {Binary,Text}{Marshaler,Unmarshaler} para torná-las mais fáceis de enviar pela rede.

Tenho certeza de que existem muitas pequenas coisas como essa que seriam bastante úteis de vez em quando.

Existem algumas ferramentas de verificação/linter para verificação exaustiva de tipos de soma simulados com interfaces. Não há razão para não haver aqueles para enums iota que informam quando os casos são perdidos ou constantes não tipadas inválidas são usadas (talvez ele deva apenas relatar qualquer coisa diferente de 0?).

Certamente há espaço para melhorias nessa frente, mesmo sem alterações de idioma.

Enums complementariam o sistema de tipos já estabelecido. Como os muitos exemplos nesta edição mostraram, os blocos de construção para enums já estão presentes. Assim como os canais são abstrações de alto nível construídas em tipos mais primitivos, as enumerações devem ser construídas da mesma maneira. Os seres humanos são arrogantes, desajeitados e esquecidos, mecanismos como enums ajudam os programadores humanos a cometer menos erros de programação.

@bep , tenho que discordar de todos os seus três pontos. As enumerações idiomáticas Go se assemelham fortemente às enumerações C, que não possuem nenhuma iteração de valores válidos, não têm nenhuma conversão automática em strings e não têm identidade necessariamente distinta.

É bom ter iteração, mas na maioria dos casos, se você quiser iteração, não há problema em definir constantes para o primeiro e o último valor. Você pode até fazê-lo de uma maneira que não exija atualização ao adicionar novos valores, pois iota automaticamente o tornará um após o fim. A situação em que o suporte ao idioma faria uma diferença significativa é quando os valores do enum não são contíguos.

A conversão automática para string é apenas um valor pequeno: especialmente nesta proposta, os valores de string precisam ser escritos para corresponder aos valores int, então há pouco a ganhar ao escrever explicitamente uma matriz de valores de string. Em uma proposta alternativa, poderia valer mais, mas também há desvantagens em forçar nomes de variáveis ​​a corresponderem a representações de strings.

Finalmente, identidade distinta, nem tenho certeza, é um recurso útil. Enums não são tipos de soma como em, digamos, Haskell. São números nomeados. Usar enums como valores de sinalizador, por exemplo, é comum. Por exemplo, você pode ter ReadWriteMode = ReadMode | WriteMode e isso é uma coisa útil. É bem possível também ter outros valores, por exemplo você pode ter DefaultMode = ReadMode . Não é como se qualquer método pudesse impedir alguém de escrever const DefaultMode = ReadMode em qualquer caso; para que serve exigir que isso aconteça em uma declaração separada?

@bep , tenho que discordar de todos os seus três pontos. As enumerações idiomáticas Go se assemelham fortemente às enumerações C, que não possuem nenhuma iteração de valores válidos, não têm nenhuma conversão automática em strings e não têm identidade necessariamente distinta.

@alercah , por favor, não coloque este idomatic Go em nenhuma discussão como um suposto "argumento vencedor"; Go não tem Enums embutidos, então falar sobre alguns idoms inexistentes faz pouco sentido.

Go foi construído para ser um C/C++ melhor ou um Java menos detalhado , então compará-lo com o último faria mais sentido. E o Java tem um Enum type embutido ("Os tipos enum da linguagem de programação Java são muito mais poderosos do que suas contrapartes em outras linguagens. "): https://docs.oracle.com/javase/tutorial/java /javaOO/enum.html

E, embora você possa discordar da "parte muito mais poderosa", o tipo Java Enum tem todos os três recursos que mencionei.

Eu posso apreciar o argumento de que Go é mais enxuto, mais simples etc., e que algum compromisso deve ser feito para mantê-lo dessa maneira, e eu vi algumas soluções hacky neste segmento que meio que funcionam, mas um conjunto de iota ints sozinhos não fazem uma enumeração.

Enumerações e conversões automáticas de strings são boas candidatas para o recurso 'ir gerar'. Já temos algumas soluções. As enumerações Java são algo no meio das enumerações clássicas e dos tipos de soma. Portanto, é um design de linguagem ruim na minha opinião.
A questão do Go idiomático é a chave, e não vejo fortes razões para copiar todos os recursos do idioma X para o idioma Y, só porque alguém está familiarizado.

Os tipos enum da linguagem de programação Java são muito mais poderosos do que suas contrapartes em outras linguagens

Isso era verdade há uma década. Veja a implementação moderna de custo zero do Option in Rust com tipos de soma e correspondência de padrões.

A questão do Go idiomático é a chave, e não vejo fortes razões para copiar todos os recursos do idioma X para o idioma Y, só porque alguém está familiarizado.

Note que não discordo muito das conclusões apresentadas aqui, mas o uso do _Go idiomático_ está colocando o Go em um pedestal artístico. A maior parte da programação de software é bastante chata e prática. E muitas vezes você só precisa preencher uma caixa suspensa com um enum ...

//go:generate enumerator Foo,Bar
Escrito uma vez, disponível em todos os lugares. Observe que o exemplo é abstrato.

@bep Acho que você leu errado o comentário original. "Go idiomática enums" deveria se referir à construção atual de usar o tipo Foo int + const-decl + iota, acredito, para não dizer "o que você está propondo não é idiomático".

@rsc Em relação ao rótulo Go2 , isso é contrário ao meu raciocínio para enviar esta proposta. #19412 é uma proposta de tipos de soma completa, que é um superconjunto mais poderoso do que minha proposta de enumeração simples aqui, e eu preferiria ver isso em Go2. Do meu ponto de vista, a probabilidade de Go2 acontecer nos próximos 5 anos é pequena, e eu prefiro ver algo acontecer em um prazo menor.

Se minha proposta de uma nova palavra-chave reservada enum for impossível para BC, ainda há outras maneiras de implementá-la, seja uma integração de linguagem completa ou ferramentas incorporadas em go vet . Como afirmei originalmente, não sou muito exigente com a sintaxe, mas acredito fortemente que seria uma adição valiosa ao Go hoje sem adicionar uma carga cognitiva significativa para novos usuários.

Uma nova palavra-chave não é possível antes do Go 2. Seria uma clara violação da garantia de compatibilidade do Go 1.

Pessoalmente, ainda não estou vendo os argumentos convincentes para enum, ou, nesse caso, para tipos de soma, mesmo para Go 2. Não estou dizendo que eles não podem acontecer. Mas um dos objetivos da linguagem Go é a simplicidade da linguagem. Não basta que um recurso de linguagem seja útil; todos os recursos da linguagem são úteis - se não fossem úteis, ninguém os proporia. Para adicionar um recurso ao Go, o recurso precisa ter casos de uso convincentes suficientes para que valha a pena complicar a linguagem. Os casos de uso mais atraentes são códigos que não podem ser escritos sem o recurso, pelo menos agora sem grande constrangimento.

Eu adoraria ver enums em Go. Estou constantemente querendo restringir minha API exposta (ou trabalhando com uma API restrita fora do meu aplicativo) na qual há um número limitado de entradas válidas. Para mim, este é o local perfeito para um enum.

Por exemplo, eu poderia estar fazendo um aplicativo cliente que se conecta a algum tipo de API de estilo RPC e tem um conjunto especificado de ações/opcodes. Eu posso usar const s para isso, mas não há nada que impeça alguém (inclusive eu!) de enviar um código inválido.

Por outro lado, se eu estiver escrevendo o lado do servidor para essa mesma API, seria bom poder escrever uma instrução switch no enum, que lançaria um erro do compilador (ou pelo menos algum go vet warnings) se todos os valores possíveis do enum não forem verificados (ou pelo menos existir um default: ).

Eu acho que isso (enums) é uma área que Swift realmente acertou .

Eu poderia estar fazendo um aplicativo cliente que se conecta a algum tipo de API de estilo RPC e tem um conjunto especificado de ações/opcodes. Eu posso usar consts para isso, mas não há nada que impeça alguém (inclusive eu!) de simplesmente enviar um código inválido.

Esta é uma ideia horrível para resolver com enums. Isso significaria que agora você nunca poderá adicionar um novo valor de enumeração, porque de repente os RPCs podem estar falhando ou seus dados se tornarão ilegíveis após a reversão. A razão pela qual o proto3 requer que o código enum gerado suporte um valor de "código desconhecido" é que esta é uma lição aprendida pela dor (compare-a com como o proto2 resolveu isso, que é melhor, mas ainda muito ruim). Você deseja que o aplicativo seja capaz de lidar com esse caso normalmente.

@Merovius Respeito sua opinião, mas discordo educadamente. Certificar-se de que apenas valores válidos sejam usados ​​é um dos principais usos de enums.

Enums não são adequados para todas as situações, mas são ótimos para algumas! O controle de versão e o tratamento de erros adequados devem ser capazes de lidar com novos valores na maioria das situações.

Para lidar com processos externos tendo um estado uh-oh é uma obrigação, certamente.

Com enums (ou os tipos de soma mais gerais e úteis), você pode adicionar um código "desconhecido" explícito à soma/enum com o qual o compilador o força a lidar (ou apenas lidar com essa situação inteiramente no terminal se tudo o que você pode fazer é registre-o e passe para a próxima solicitação).

Acho os tipos de soma mais úteis para dentro de um processo quando sei que há X casos com os quais sei que devo lidar. Para X pequeno não é difícil de gerenciar, mas, para X grande, eu aprecio o compilador gritando comigo, especialmente quando refatorando.

Além dos limites da API, os casos de uso são menores e deve-se sempre errar no lado da extensibilidade, mas às vezes você tem algo que realmente só pode ser uma das coisas X, como com um AST ou exemplos mais triviais como um "dia de o valor da semana" onde o intervalo é praticamente estabelecido neste ponto (até a escolha do sistema de calendário).

@jimmyfrasche Eu posso te dar o Dia da Semana, mas não o AST. As gramáticas evoluem. O que pode ser inválido hoje, pode ser totalmente válido amanhã e isso pode envolver a adição de novos tipos de nó ao AST. Com tipos de soma verificados pelo compilador, isso não seria possível sem interrupções.

E não vejo por que isso não pode ser resolvido apenas por um exame veterinário; dando-lhe uma verificação estática perfeitamente adequada de casos exaustivos e me dando a possibilidade de reparos graduais.

Estou brincando com a implementação de um cliente para uma API de servidor. Alguns dos argumentos e valores de retorno são enumerações na API. Existem 45 tipos de enumeração no total.

Usar constantes enumeradas em Go não é viável no meu caso, pois alguns dos valores para diferentes tipos de enumeração compartilham o mesmo nome. No exemplo abaixo, Destroy aparece duas vezes para que o compilador emita o erro Destroy redeclared in this block .

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

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

Por isso, vou precisar de uma representação diferente. Idealmente, um que permita que um IDE mostre os valores possíveis para um determinado tipo para que os usuários do cliente tenham mais facilidade em usá-lo. Ter enum como cidadão de primeira classe em Go satisfaria isso.

@kongslund Eu sei que não é uma implementação perfeita, mas acabei de criar um gerador de código que pode ser do seu interesse. Requer apenas que você declare seu enum em um comentário acima da declaração de tipo e gerará o resto para você.

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

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

Geraria

const(
  _ TaskAllowedOperations = iota
  TaskAllowedOperationsCancel
  TaskAllowedOperationsDestroy
)

const(
  _ OnNormalExit = iota
  OnNormalExitDestroy
  OnNormalExitRestart
)

A melhor parte é que ele geraria métodos String() que excluem o prefixo neles, permitindo que você analise "Destroy" como TaskAllowedOperations ou OnNormalExit .

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

Agora que o plug está fora do caminho...

Pessoalmente, não me importo que enums não sejam incluídos como parte da linguagem go, o que não era meu sentimento original em relação ao assunto. Quando cheguei pela primeira vez, muitas vezes tive uma reação confusa sobre por que tantas escolhas foram feitas. Mas depois de usar a linguagem, é bom ter a simplicidade à qual ela adere e, se algo extra for necessário, é provável que outra pessoa também precise e tenha feito um pacote incrível para ajudar com esse problema específico. Mantendo a quantidade de cruft a meu critério.

Muitos pontos válidos foram levantados nesta discussão, alguns a favor do suporte enum e também muitos contra (pelo menos até onde a proposta disse algo sobre o que são "enums" em primeiro lugar). Algumas coisas que me marcaram:

  • O exemplo introdutório (Enums in Go hoje) é enganoso: esse código é gerado e quase ninguém escreveria código Go assim à mão. Na verdade, a sugestão (como pode ser com suporte a idiomas) está muito mais próxima do que já fazemos em Go.

  • @jediorange menciona que Swift "realmente acertou (enums)": Seja como for, mas enums Swift são uma fera surpreendentemente complicada, misturando todos os tipos de conceitos. Em Go, evitamos deliberadamente mecanismos que se sobrepõem a outros recursos da linguagem e, em troca, obtemos mais ortogonalidade. A consequência para um programador é que ele não precisa decidir qual recurso usar: um enum ou uma classe, ou um tipo de soma (se os tivermos), ou uma interface.

  • O ponto de @ianlancetaylor sobre a utilidade dos recursos da linguagem não deve ser encarado de ânimo leve. Há um zilhão de recursos úteis; a questão é quais são realmente atraentes e valem seu custo (de complexidade extra da linguagem e, portanto, legibilidade e implementação).

  • Como um ponto menor, constantes definidas por iota em Go obviamente não são restritas a ints. Desde que sejam constantes, eles são restritos a (possivelmente nomeados) tipos básicos (incluindo floats, booleans, strings: https://play.golang.org/p/lhd3jqqg5z).

  • @merovius faz bons comentários sobre as limitações das verificações em tempo de compilação (estática!). Tenho muita dúvida de que enumerações que não podem ser estendidas sejam adequadas em suturas onde a extensão é desejável ou esperada (qualquer superfície de API de longa duração evolui com o tempo).

O que me leva a algumas questões sobre esta proposta que acredito que precisam ser respondidas antes que possa haver algum progresso significativo:

1) Quais são as expectativas reais para enums conforme proposto? @bep menciona enumerabilidade, iterabilidade, representações de strings, identidade. Há mais? Há menos?

2) Assumindo a lista em 1), as enumerações podem ser estendidas? Se sim, como? (no mesmo pacote? outro pacote?) Se não podem ser estendidos, por que não? Por que isso não é um problema na prática?

3) Namespace: No Swift, um tipo enum introduz um novo namespace. Há um mecanismo significativo (açúcar sintático, dedução de tipo) de modo que o nome do namespace não precisa ser repetido em todos os lugares. Por exemplo, para valores enum de um enum Mês, no contexto correto, pode-se escrever .Janeiro em vez de Mês.Janeiro (ou pior, MeuPacote.Mês.Janeiro). Um namespace enum é necessário? Em caso afirmativo, como um namespace enum é estendido? Que tipo de açúcar sintático é necessário para fazer isso funcionar na prática?

4) Os valores enum são constantes? Valores imutáveis?

5) Que tipo de operações são possíveis em valores enum (digamos, além da iteração): Posso mover um para frente, outro para trás? Requer funções ou operadores embutidos extras? (Nem todas as iterações podem estar em ordem). O que acontece se alguém avançar além do último valor enum? Isso é um erro de tempo de execução?

(Eu corrigi minha redação do próximo parágrafo em https://github.com/golang/go/issues/19814#issuecomment-322771922. Desculpas pela escolha descuidada de palavras abaixo.)

Sem tentar realmente responder a essas perguntas, esta proposta não tem sentido ("Eu quero enums que façam o que eu quero" não é uma proposta).

Sem tentar realmente responder a essas perguntas, esta proposta não tem sentido

@griesemer Você tem um ótimo conjunto de pontos / perguntas - mas rotular essa proposta sem sentido por não responder a essas perguntas faz pouco sentido. A barra para contribuição é alta neste projeto, mas deveria ser permitido _propor algo_ sem ter um doutorado em compiladores, e uma proposta não deveria precisar ser um projeto _pronto para implementar_.

  • Go precisava dessa proposta, pois iniciou uma discussão muito necessária => valor e significado
  • Se também levar à proposta #21473 => valor e significado

O exemplo introdutório (Enums in Go hoje) é enganoso: esse código é gerado e quase ninguém escreveria código Go assim à mão. Na verdade, a sugestão (como pode ser com suporte a idiomas) está muito mais próxima do que já fazemos em Go.

@griesemer tenho que discordar. Eu não deveria ter deixado todas as letras maiúsculas no nome da variável Go, mas há muitos lugares onde o código manuscrito parece quase idêntico à minha sugestão, escrita por Googlers que eu respeito na comunidade Go. Seguimos o mesmo padrão em nossa base de código com bastante frequência. Aqui está um exemplo extraído da biblioteca do Google Cloud Go.

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

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

Eles usam a mesma construção em vários lugares.
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

Houve alguma discussão mais tarde sobre como você pode tornar as coisas mais concisas se estiver bem usando iota , o que pode ser útil por si só, mas para um caso de uso limitado. Veja meu comentário anterior para mais detalhes. https://github.com/golang/go/issues/19814#issuecomment -290948187

@bep Ponto justo; Peço desculpas pela minha escolha descuidada de palavras. Deixe-me tentar de novo, espero formular meu último parágrafo acima de forma mais respeitosa e mais clara desta vez:

Para poder fazer progressos significativos, acredito que os proponentes desta proposta devem tentar ser um pouco mais precisos sobre o que eles acreditam ser características importantes de enums (por exemplo, respondendo a algumas das perguntas em https://github. com/golang/go/issues/19814#issuecomment-322752526). Da discussão até agora, os recursos desejados são descritos apenas de forma bastante vaga.

Talvez como primeiro passo, seria realmente útil ter estudos de caso que mostrassem como o Go existente fica (significativamente) aquém e como enums resolveriam um problema melhor/mais rápido/mais claro, etc. Veja também a excelente palestra de @rsc no Gophercon em relação às alterações de idioma do Go2.

@derekperkins Eu chamaria essas definições constantes (digitadas), não enums. Suponho que nosso desacordo se deve a uma compreensão diferente do que um "enum" deveria ser, daí minhas perguntas acima.

(Meu https://github.com/golang/go/issues/19814#issuecomment-322774830 anterior deveria ter ido para @derekperkins , é claro, não @derekparker. Autocomplete me derrotou.)

A julgar pelo comentário @derekperkins e respondendo parcialmente às minhas próprias perguntas, entendo que um "enum" Go deve ter pelo menos as seguintes qualidades:

  • capacidade de agrupar um conjunto de valores em um (novo) tipo
  • facilitar a declaração de nomes (e valores correspondentes, se houver) com esse tipo, com sobrecarga sintática mínima ou clichê
  • capacidade de iterar por esses valores em ordem crescente de declaração

Isso soa certo? Em caso afirmativo, o que mais precisa ser adicionado a esta lista?

Suas perguntas são todas boas.

Quais são as expectativas reais para enums conforme proposto? @bep menciona enumerabilidade, iterabilidade, representações de strings, identidade. Há mais? Há menos?

Assumindo a lista em 1), as enumerações podem ser estendidas? Se sim, como? (no mesmo pacote? outro pacote?) Se não podem ser estendidos, por que não? Por que isso não é um problema na prática?

Eu não acho que enums podem ser estendidos por dois motivos:

  1. Enums devem representar todo o intervalo de valores aceitáveis, portanto, estendê-los não faz sentido.
  2. Assim como os tipos Go normais não podem ser estendidos em pacotes externos, isso mantém as mesmas mecânicas e expectativas do desenvolvedor

Namespace: No Swift, um tipo enum introduz um novo namespace. Há um mecanismo significativo (açúcar sintático, dedução de tipo) de modo que o nome do namespace não precisa ser repetido em todos os lugares. Por exemplo, para valores enum de um enum Mês, no contexto correto, pode-se escrever .Janeiro em vez de Mês.Janeiro (ou pior, MeuPacote.Mês.Janeiro). Um namespace enum é necessário? Em caso afirmativo, como um namespace enum é estendido? Que tipo de açúcar sintático é necessário para fazer isso funcionar na prática?

Eu entendo como surgiu o namespace, pois todos os exemplos que mencionei prefixam com o nome do tipo. Embora eu não me oponha se alguém se sentir forte em adicionar namespaces, acho que isso está fora do escopo desta proposta. A prefixação se encaixa perfeitamente no sistema atual.

Os valores enum são constantes? Valores imutáveis?

Eu pensaria em constantes.

Que tipo de operações são possíveis em valores enum (digamos, além da iteração): Posso mover um para frente, outro para trás? Requer funções ou operadores embutidos extras? (Nem todas as iterações podem estar em ordem). O que acontece se alguém avançar além do último valor enum? Isso é um erro de tempo de execução?

Eu usaria como padrão as práticas Go padrão para fatias/matrizes (não mapas). Os valores de enumeração seriam iteráveis ​​com base na ordem da declaração. No mínimo, haveria suporte de alcance. Eu me afasto de deixar enums serem acessados ​​via índice, mas não me sinto muito bem com isso. Não oferecer suporte a isso deve eliminar o possível erro de tempo de execução.

Haveria um novo erro de tempo de execução (pânico?) causado pela atribuição de um valor inválido a uma enumeração, seja por atribuição direta ou conversão de tipo.

Se eu resumir isso corretamente, os valores enum conforme você os propõe são como constantes digitadas (e, como constantes, podem ter valores constantes definidos pelo usuário), mas:

  • eles também definem o tipo enum associado aos valores enum (caso contrário, eles são apenas constantes) na mesma declaração
  • é impossível converter/criar um valor enum de um tipo enum existente fora de sua declaração
  • é possível iterar sobre eles

Isso soa certo? (Isso corresponderia à abordagem clássica que as linguagens têm adotado para enums, iniciada há cerca de 45 anos por Pascal).

Sim, é exatamente isso que estou propondo.

E quanto aos comandos switch? AIUI que é um dos principais impulsionadores da proposta.

Ser capaz de ativar uma enumeração está implícito, eu acho, já que você pode ativar basicamente qualquer coisa. Eu gosto que o swift tenha erros se você não satisfez totalmente o enum no seu switch, mas isso pode ser tratado pelo veterinário

@jediorange Eu estava me referindo especificamente à questão dessa última parte, se deve ou não haver uma verificação de exaustividade (no interesse de manter a proposta completa). "Não" é, é claro, uma resposta perfeita.

A mensagem original desta edição menciona os protobufs como motivadores. Eu gostaria de chamar explicitamente que com a semântica dada agora, o compilador protobuf precisaria criar um caso "não reconhecido" adicional para qualquer enum (implicando algum esquema de desmembramento de nomes para evitar colisões). Ele também precisaria adicionar um campo adicional a qualquer estrutura gerada usando enums (novamente, deformando nomes de alguma forma), caso o valor de enum decodificado não esteja no intervalo compilado. Assim como é feito atualmente para java . Ou, provavelmente, continue a usar int s.

@Merovius Minha proposta original mencionou protobufs como exemplo, não como o principal motivador da proposta. Você traz um bom ponto sobre essa integração. Acho que provavelmente deveria ser tratado como uma preocupação ortogonal. A maioria dos códigos que eu vi converte dos tipos de protobuf gerados em estruturas de nível de aplicativo, preferindo usá-los internamente. Faria sentido para mim que o protobuf pudesse continuar inalterado e, se os criadores de aplicativos quisessem convertê-los em um Go enum, eles poderiam lidar com os casos extremos que você apresenta no processo de conversão.

@derekperkins Mais algumas perguntas:

  • Qual é o valor zero para uma variável do tipo enum que não é explicitamente inicializada? Suponho que não possa ser zero em geral (o que complica a alocação/inicialização de memória).

  • Podemos fazer aritmética limitada com valores enum? Por exemplo, em Pascal (no qual programei uma vez, há muito tempo), era surpreendentemente necessário iterar em etapas > 1. E às vezes alguém queria calcular o valor enum.

  • Em relação à iteração, por que o suporte à iteração produzida (e stringify) não é bom o suficiente?

Qual é o valor zero para uma variável do tipo enum que não é explicitamente inicializada? Suponho que não possa ser zero em geral (o que complica a alocação/inicialização de memória).

Como mencionei na proposta inicial, esta é uma das decisões mais difíceis de tomar. Se a ordem de definição for importante para a iteração, acho que faria sentido ter o primeiro valor definido como padrão.

Podemos fazer aritmética limitada com valores enum? Por exemplo, em Pascal (no qual programei uma vez, há muito tempo), era surpreendentemente necessário iterar em etapas > 1. E às vezes alguém queria calcular o valor enum.

Se você estiver usando enums numéricos ou baseados em string, isso significa que todos os enums têm um índice implícito baseado em zero? A razão que eu mencionei antes que eu me inclino para apenas iterações range suportadas e não baseadas em índice, é que não expõe a implementação subjacente, que poderia usar uma matriz ou um mapa ou qualquer outra coisa por baixo. Eu não prevejo a necessidade de acessar enums via índice, mas se você tiver razões pelas quais isso seria benéfico, não acho que haja uma razão para não permitir isso.

Em relação à iteração, por que o suporte à iteração produzida (e stringify) não é bom o suficiente?

A iteração não é meu caso de uso principal pessoalmente, embora eu ache que agrega valor à proposta. Se esse fosse o fator determinante, talvez go generate fosse suficiente. Isso não ajuda a garantir a segurança do valor. O argumento Stringer() assume que o valor bruto será iota ou int ou algum outro tipo que represente o valor "real". Você também teria que gerar (Un)MarshalJSON , (Un)MarshalBinary , Scanner/Valuer e quaisquer outros métodos de serialização que você possa usar para garantir que o Stringer foi usado para se comunicar vs. o que o Go usa internamente.

@griesemer Acho que posso não ter respondido totalmente à sua pergunta sobre a extensibilidade de enums, pelo menos no que diz respeito à adição/remoção de valores. Ter a capacidade de editá-los é uma parte essencial desta proposta.

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

qualquer pacote que queira alterar um conjunto de enums, quebraria automática e forçosamente todos os seus importadores

Eu não vejo como isso é diferente de qualquer outra mudança de API. Cabe ao criador do pacote tratar respeitosamente o BC, da mesma forma que se os tipos, funções ou assinaturas de funções mudarem.

De uma perspectiva de implementação, seria bastante complexo oferecer suporte a tipos cujo valor padrão não fosse todos os bits zero. Não existem esses tipos hoje. Exigir tal recurso teria que contar como uma nota contra essa ideia.

A única razão pela qual o idioma requer make para criar um canal é preservar esse recurso para os tipos de canal. Caso contrário make pode ser opcional, usado apenas para definir o tamanho do buffer do canal ou para atribuir um novo canal a uma variável existente.

@derekperkins A maioria das outras alterações de API pode ser orquestrada para reparo gradual. Eu realmente recomendo ler a descrição de Russ Cox, deixa muitas coisas muito claras.

As enumerações abertas (como a construção const+iota atual) permitem o reparo gradual, por (por exemplo) a) definindo o novo valor sem usá-lo, b) atualizando dependências reversas para manipular o novo valor, c) começando a usar o valor. Ou, se você deseja remover um valor, a) pare de usar o valor, b) atualize as dependências reversas para não mencionar o valor a ser removido, c) remova o valor.

Com enumerações fechadas (verificadas pelo compilador) isso não é possível. Se você remover a manipulação de um valor ou definir um novo, o compilador reclamará imediatamente sobre um switch-case ausente. E você não pode adicionar manipulação de um valor antes de definir um.

A questão não é se as alterações individuais podem ser consideradas inválidas (elas podem, isoladamente), mas se existe uma sequência ininterrupta de commits sobre a base de código distribuída que não seja interrompida.

De uma perspectiva de implementação, seria bastante complexo oferecer suporte a tipos cujo valor padrão não fosse todos os bits zero. Não existem esses tipos hoje. Exigir tal recurso teria que contar como uma nota contra essa ideia.

@ianlancetaylor Definitivamente não serei capaz de falar sobre a implementação completa, mas se enums fossem implementados como um array baseado em 0 (que é o que parece ser a favor de @griesemer ), então 0 como o índice parece poderia dobrar como "todos os bits-zero".

Com enumerações fechadas (verificadas pelo compilador) isso não é possível.

@Merovius Se a exaustividade fosse verificada por go vet ou ferramentas semelhantes, conforme sugerido por @jediorange versus aplicado pelo compilador, isso aliviaria suas preocupações?

@derekperkins Sobre sua nocividade, sim. Não sobre sua falta de utilidade. Os mesmos problemas de distorção de versão também acontecem para a maioria dos casos de uso para os quais eles geralmente são considerados (syscalls, protocolos de rede, formatos de arquivo, objetos compartilhados…). Há uma razão pela qual o proto3 requer enums abertos e o proto2 não - é uma lição aprendida com muitas interrupções e incidentes de corrupção de dados. Mesmo que o Google já seja bastante cuidadoso para evitar distorções de versão. Da minha perspectiva, enums abertos com casos padrão são apenas a solução correta. E além da suposta segurança contra valores inválidos, eles não trazem muito para a mesa, pelo que posso dizer.

Tudo isso dito, eu não sou um decisor.

@derekperkins Em https://github.com/golang/go/issues/19814#issuecomment -322818206 você está confirmando que (do seu ponto de vista):

  • uma declaração enum declara um tipo enum junto com valores enum nomeados (constantes)
  • é possível iterar sobre eles
  • nenhum valor pode ser adicionado ao enum fora de sua declaração
    E mais tarde: uma troca de enums deve ser (ou talvez não) exaustiva (parece menos importante)

Em https://github.com/golang/go/issues/19814#issuecomment -322895247 você está dizendo que:

  • o primeiro valor definido provavelmente deve ser o valor padrão (zero) (observe que isso não importa para a iteração, é importante para a inicialização da variável enum)
  • iteração não é sua principal motivação

E em https://github.com/golang/go/issues/19814#issuecomment -322903714 você está dizendo que a "capacidade de editá-los é uma parte importante desta proposta".

Estou confuso: então a iteração não é um motivador primário, tudo bem. Isso deixa no mínimo uma declaração enum de valores enum que são constantes e não podem ser estendidos para fora da declaração. Mas agora você está dizendo que a capacidade de editá-los é importante. O que isso significa? Certamente não que eles possam ser estendidos (isso seria uma contradição). São variáveis? (Mas então eles não são constantes).

Em https://github.com/golang/go/issues/19814#issuecomment -322903714 você está dizendo que enums podem ser implementados como array baseado em 0. Isso sugere que uma declaração de enum introduz um novo tipo junto com uma lista ordenada de nomes de enum que são índices constantes baseados em 0 em uma matriz de valores de enum (para os quais o espaço é reservado automaticamente). É isso que você quer dizer? Em caso afirmativo, por que não seria suficiente apenas declarar um array de tamanho fixo e uma lista de índices constantes para acompanhá-lo? A verificação dos limites da matriz garantiria automaticamente que você não pudesse "estender" o intervalo de enumeração e a iteração já seria possível.

o que estou perdendo?

Estou confuso: então a iteração não é um motivador primário, tudo bem.

Eu tenho minhas próprias razões pelas quais quero enums, ao mesmo tempo em que tento levar em consideração o que outros neste tópico, incluindo @bep e outros, expressaram como partes necessárias da proposta.

Isso deixa no mínimo uma declaração enum de valores enum que são constantes e não podem ser estendidos para fora da declaração. Mas agora você está dizendo que a capacidade de editá-los é importante. O que isso significa? Certamente não que eles possam ser estendidos (isso seria uma contradição). São variáveis? (Mas então eles não são constantes).

Quando digo para editá-los, é para o ponto de @Merovius que eles são enums abertos. Constantes em tempo de construção, mas não bloqueadas para sempre.

Em #19814 (comentário) você está dizendo que enums podem ser implementados como array baseado em 0.

Isso sou apenas eu especulando além do meu paygrade sobre como imagino que ele possa ser implementado nos bastidores, com base em seu https://github.com/golang/go/issues/19814#issuecomment -322884746 e https de @ianlancetaylor : //github.com/golang/go/issues/19814#issuecomment -322899668

"Podemos fazer aritmética limitada com valores de enumeração? Por exemplo, em Pascal (no qual programei uma vez, há muito tempo), era surpreendentemente necessário iterar em etapas > 1. E às vezes alguém queria calcular o valor de enumeração."

Não sei como você planeja fazer isso para qualquer enum não inteiro, daí minha pergunta sobre se essa aritmética exigiria que cada membro do enum recebesse um índice implicitamente com base na ordem da declaração.

De uma perspectiva de implementação, seria bastante complexo oferecer suporte a tipos cujo valor padrão não fosse todos os bits zero. Não existem esses tipos hoje. Exigir tal recurso teria que contar como uma nota contra essa ideia.

Novamente, eu não sei como o compilador funciona, então eu estava apenas tentando continuar a conversa. No final das contas, não estou tentando propor nada radical. Como você mencionou antes, "Isso corresponderia à abordagem clássica que as linguagens adotaram para enums, iniciadas há cerca de 45 anos por Pascal", e isso se encaixa no projeto.

A qualquer pessoa que tenha manifestado interesse, sinta-se à vontade para contribuir.

Outra questão é se é possível usar essas enumerações para indexar em matrizes ou fatias. Uma fatia geralmente é uma maneira muito eficiente e compacta de representar um mapeamento enum->value e exigir um mapa seria lamentável, eu acho.

@derekperkins Ok, estou preocupado que isso nos coloque (ou pelo menos a mim) de volta à estaca zero: qual é o problema que você está tentando resolver? Você simplesmente quer uma maneira melhor de escrever o que fazemos atualmente com constantes e talvez iota (e para o qual usamos go generate para obter representações de string)? Ou seja, algum açúcar sintático para uma notação que você (talvez) ache excessivamente pesada? (Essa é uma boa resposta, apenas tentando entender.)

Você mencionou que tem seus próprios motivos para desejá-los, talvez possa explicar um pouco mais quais são esses motivos. O exemplo que você deu no início não faz muito sentido para mim, mas provavelmente está faltando alguma coisa.

Do jeito que está, todo mundo tem uma compreensão um pouco diferente do que essa proposta ("enums") implica, como ficou claro pelas várias respostas: Há uma enorme variedade de possibilidades entre enums Pascal e Swift. A menos que você (ou outra pessoa) descreva muito claramente o que é proposto (não estou pedindo uma implementação, veja bem), será difícil fazer algum progresso significativo ou até mesmo debater os méritos dessa proposta.

Isso faz sentido?

@griesemer Faz todo o sentido e eu entendo a barra a ser ultrapassada que @rsc falou na Gophercon. Você obviamente tem uma compreensão muito mais profunda do que eu jamais terei. Em #21473, você mencionou que iota para vars não foi implementado porque não havia um caso de uso convincente na época. É a mesma razão que enum não foi incluído desde o início? Eu estaria muito interessado em saber sua opinião sobre se isso agregaria ou não valor ao Go, e se agregasse, por onde você iniciaria o processo?

@derekperkins Em relação à sua pergunta em https://github.com/golang/go/issues/19814#issuecomment -323144075: Na época (no design do Go), estávamos considerando apenas enums relativamente simples (digamos, estilo Pascal ou C). Não me lembro de todos os detalhes, mas certamente havia uma sensação de que não havia benefício suficiente para o maquinário extra necessário para enums. Sentimos que eram, essencialmente, declarações constantes glorificadas.

Também há problemas com essas enumerações tradicionais: é possível fazer aritmética com elas (são apenas números inteiros), mas o que significa se elas "foram do intervalo (enum)"? Em Go eles são apenas constantes e "fora de alcance" não existe. Outra é a iteração: em Pascal havia funções internas especiais (acho que SUCC e PRED) para avançar o valor de uma variável do tipo enum para frente e para trás (em C, basta fazer ++ ou --). Mas o mesmo problema aparece aqui também: O que acontece se passar do final (problema muito comum em um loop for variando sobre valores enum usando ++ ou o SUCC equivalente em Pascal). Finalmente, uma declaração enum introduz um novo tipo, cujos elementos são os valores enum. Esses valores têm nomes (os definidos na declaração enum), mas esses nomes estão (em Pascal, C) no mesmo escopo que o tipo. Isso é um pouco insatisfatório: ao declarar duas enumerações diferentes, seria de esperar que se pudesse usar o mesmo nome de valor de enumeração para cada tipo de enumeração sem conflito, o que não é possível. É claro que Go também não resolve isso, mas uma declaração constante também não parece estar introduzindo um novo namespace. Uma solução melhor é introduzir um espaço de nomes com cada enumeração, mas cada vez que um valor enum é usado, ele precisa ser qualificado com o nome do tipo enum, o que é irritante. O Swift resolve isso deduzindo o tipo enum sempre que possível e, em seguida, pode-se usar o nome do valor enum prefixado com um ponto. Mas isso é um pouco de maquinaria. E finalmente, às vezes (muitas vezes, em APIs públicas), é preciso estender uma declaração enum. Se isso não for possível (você não possui o código), há um problema. Com constantes, esses problemas não existem.

Provavelmente há mais do que isso; isso é apenas o que vem à mente. No final, decidimos que era melhor emular enums em Go usando as ferramentas ortogonais que já tínhamos: tipos inteiros personalizados que tornam atribuições errôneas menos prováveis ​​e o mecanismo iota (e a capacidade de deixar de lado expressões de inicialização repetidas) para o açúcar sintático.

Daí minhas perguntas: O que você quer ganhar com declarações de enumeração especializadas que não podemos emular adequadamente em Go com pouca sobrecarga sintática? Posso pensar em enumeração e em um tipo de enumeração que não pode ser estendido fora da declaração. Eu posso pensar em mais habilidades para valores enum, como em Swift.

A enumeração pode ser resolvida facilmente com um gerador de Go em Go. Já temos uma longarina. Restringir a extensão é problemático nos limites da API. Mais habilidades para valores de enumeração (digamos como em Swift) parecem muito diferentes de Go porque mistura muitos conceitos ortogonais. Em Go, provavelmente conseguiríamos isso usando blocos de construção elementares.

@griesemer Obrigado pela sua resposta atenciosa. Não discordo que sejam basicamente declarações constantes glorificadas. Ter segurança de tipo em Go é ótimo, e o principal valor que enum me forneceria pessoalmente é segurança de valor. A maneira de imitar isso em Go hoje é executar funções de validação em cada ponto de entrada para essa variável. É verboso e torna fácil cometer erros, mas é possível com a linguagem como é hoje. Eu já coloquei o namespace prefixando o nome do tipo na frente do enum, que, embora detalhado, não é grande coisa.

Eu pessoalmente não gosto da maioria dos usos para iota . Embora legal, na maioria das vezes meus valores enum -like mapeiam para recursos externos como um db ou uma API externa, e eu prefiro ser mais explícito que o valor não deve ser alterado se você reordenar. iota também não ajuda na maioria dos lugares em que eu usaria um enum porque eu estaria usando uma lista de valores de string.

No final das contas, não sei quanto mais posso esclarecer essa proposta. Eu adoraria se eles fossem apoiados de qualquer forma que fizesse sentido para o Go. Independentemente da implementação exata, eu ainda poderia usá-los e eles tornariam meu código mais seguro.

Acho que a maneira canônica que o Go faz enums hoje (como visto em https://github.com/golang/go/issues/19814#issuecomment-290909885) está bem próxima da correta.
Existem algumas desvantagens:

  1. Eles não podem ser iterados
  2. Eles não têm representação de string
  3. Os clientes podem introduzir valores enum falsos

Estou bem sem o número 1.
go:generate + stringer pode ser usado para #2. Se isso não resolver o seu caso de uso, torne o tipo base do seu "enum" uma string em vez de um int e use valores constantes de string.

3 é o mais difícil de lidar com o Go de hoje. Eu tenho uma proposta boba que pode lidar bem com isso.

Adicione uma palavra-chave explicit a uma definição de tipo. Essa palavra-chave proíbe conversões nesse tipo, exceto para conversões em blocos const no pacote no qual esse tipo está definido. (Ou restricted ? Ou talvez enum significa explicit type ?)

Reutilizando o exemplo que mencionei acima,

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

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

Há conversões de int para SearchRequest dentro do bloco const . Mas apenas o autor do pacote pode introduzir um novo valor SearchRequest, e é improvável que introduza um acidentalmente (passando um int para uma função que espera um SearchRequest , por exemplo).

Não estou propondo essa solução ativamente, mas acho que o não-construir-acidentalmente-um-inválido é a propriedade saliente de enums que não podem ser capturadas em Go hoje (a menos que você vá para o struct com um rota de campo não exportada).

Eu acho que o risco interessante com enums é com constantes não tipadas. As pessoas que escrevem uma conversão de tipo explícita sabem o que estão fazendo. Eu estaria disposto a considerar uma maneira de Go proibir conversões de tipo explícitas em determinadas circunstâncias, mas acho que isso é totalmente ortogonal à noção de tipos de enumeração. É uma ideia que se aplica a qualquer tipo de tipo.

Mas constantes não tipadas podem levar à criação acidental e inesperada de um valor do tipo, de uma forma que não é verdade para conversões de tipo explícito. Então eu acho que a sugestão de @randall77 de explicit pode ser simplificada para simplesmente significar que constantes não tipadas podem não ser convertidas implicitamente para o tipo. Uma conversão de tipo explícita é sempre necessária.

Eu estaria disposto a considerar uma maneira de o Go proibir conversões de tipo explícito em determinadas circunstâncias

@ianlancetaylor Opcionalmente, não permitir conversões de tipo, explícitas ou implícitas, resolveria os problemas que me levaram a criar essa proposta em primeiro lugar. Somente o pacote de origem seria capaz de criar e, assim, satisfazer quaisquer tipos. Isso é ainda melhor do que a solução enum em alguns aspectos porque não só suporta declarações const , mas qualquer tipo.

@randall77 , @ianlancetaylor Como fazer a sugestão explicit funcionar junto com o valor zero desse tipo?

@derekperkins Não permitir conversões de tipo completamente tornará impossível usar esses tipos em codificadores/decodificadores genéricos, como os pacotes encoding/* .

@Merovius Acho que @ianlancetaylor sugere a restrição apenas para conversões implícitas (por exemplo, atribuição de uma constante não tipada a um tipo restrito). A conversão explícita ainda seria possível.

@griesemer eu sei :) Mas entendi que @derekperkins sugeriu de forma diferente.

A permissão de conversões explícitas não prejudica o motivo pelo qual estamos pensando nesse qualificador "explícito"? Se alguém pode decidir converter um valor arbitrário em um tipo "explícito", não temos mais garantias de que um determinado valor é uma das constantes enumeradas do que temos agora.

Acho que ajuda para o uso casual ou não intencional de constantes não tipadas, que talvez seja a coisa mais importante.

Suponho que estou questionando se não permitir conversões explícitas está no "espírito de Go". Não permitir conversões explícitas está dando um grande passo em direção à programação baseada em tipos, em vez de programação baseada na escrita de código. Acho que Go está tomando uma posição clara a favor do último.

@griesemer @Merovius Vou repostar a citação de @ianlancetaylor novamente, já que foi sugestão dele, não minha.

Eu estaria disposto a considerar uma maneira de o Go proibir conversões de tipo explícito em determinadas circunstâncias

Tanto @rogpeppe quanto @Merovius trazem bons pontos sobre as ramificações. Permitir conversões explícitas, mas não conversões implícitas, não resolve o problema de garantir tipos válidos, mas perder a codificação genérica seria uma grande desvantagem.

Houve muitas idas e vindas aqui, mas acho que houve algumas boas ideias. Aqui está um resumo do que eu gostaria de ver (ou algo semelhante), que parece se alinhar com o que alguns outros disseram. Admito abertamente que não sou designer de linguagens ou programador de compiladores, então não sei se funcionaria bem.

  1. Enums enraizados apenas em tipos básicos (string, uint, int, rune, etc). Se nenhum tipo de base for necessário, o padrão pode ser uint?
  2. Todos os valores válidos do enum devem ser declarados com a declaração de tipo -- Constantes. Valores inválidos (não declarados na declaração de tipo) não podem ser convertidos no tipo enum.
  3. Representação automática de strings para depuração (é bom ter).
  4. Verificações em tempo de compilação quanto à exaustividade nas instruções switch na enumeração. Opcionalmente, recomende (via go vet ?) um caso default , mesmo quando já exaustivo (provavelmente um erro) para alterações futuras.
  5. O valor zero deve ser essencialmente inválido (não algo na declaração enum). Eu pessoalmente gostaria que fosse nil , como uma fatia faz.

Esse último _pode_ ser um pouco controverso. E eu não sei ao certo se isso funcionaria, mas acho que se encaixaria semanticamente - assim como alguém verificaria uma fatia de nil , poderia colocar verificações de nil valor enum.

Quanto à iteração, acho que nunca usaria, mas não vejo mal nisso.

Como exemplo de como poderia ser declarado:

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

Além disso, o estilo Swift de inferir o tipo e usar "sintaxe de ponto" seria _legal_, mas definitivamente não é necessário.

digite EnumA int
const (
Desconhecido EnumA = iota
AAA
)


digite EnumB int
const (
Desconhecido EnumB = iota
BBB
)

Existem 2 pedaços de código que não podem existir em um único arquivo Go, nem no mesmo pacote, nem mesmo um é importado de outro pacote.

Por favor, apenas implemente a maneira C# de implementar o Enum:
digite Dias enum {Sáb, Dom, Seg, Ter, Qua, Qui, Sex}
type Days enum[int] { Sáb:1 , Dom, Ter, Qua, Qui, Sex}
type Days enum[string] { Sáb: "Saturado", Dom:"Domingo" etc}

@KamyarM Como isso é melhor do que

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

e

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

Gostaria de solicitar a restrição de comentários a novas abordagens/argumentos. Muitas pessoas estão inscritas neste tópico e adicionar ruído/repetição pode ser percebido como desrespeito ao seu tempo e atenção. Há muita discussão lá em cima ↑, incluindo respostas detalhadas para ambos os comentários anteriores. Você não concordará com tudo o que foi dito e nenhum dos lados pode gostar dos resultados dessa discussão até agora - mas simplesmente ignorá-la também não ajudará a movê-la em uma direção produtiva.

É melhor porque não tem problema de conflito de nomenclatura. Também suporta verificação de tipo de compilador. A abordagem que você mencionou organizou melhor do que nada, mas o compilador não restringe você sobre o que você pode atribuir a ele. Você pode atribuir um inteiro que não seja nenhum dos dias a um objeto desse tipo:
var a dias
a = 10
compilador realmente não faz nada sobre isso. Portanto, não há muito sentido para esse tipo de enum. além de ser melhor organizado em IDEs como GoLand

gostaria de ver algo assim

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

Ou com uso automático de 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
}

Isso proporcionará simplicidade e facilidade de uso:

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

Além disso, enum deve ter um método interno para validar o valor para que possamos validar algo da entrada do usuário:

if day in WeekDay {
  makeItWorkOn(day)
}

E coisas simples como:

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

Para ser honesto, minha sintaxe favorita seria assim (KISS):

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

@zoonman O último exemplo não segue o seguinte princípio de Go: uma declaração de função começa com func , uma declaração de tipo começa com type , uma declaração de variável começa com var , ...

@ md2perpe Não estou tentando seguir os princípios de "tipo" de Go, estou escrevendo código todos os dias e o único princípio que sigo - manter as coisas simples.
Do que mais código você tem que escrever _para seguir princípios_ então mais tempo é desperdiçado.
TBH sou novato em Go, mas há muitas coisas que posso criticar.
Por exemplo:

struct User {
  Id uint
  Email string
}

É mais fácil de escrever e entender do que

type User struct {
  Id uint
  Email string
}

Posso dar um exemplo de onde o tipo deve ser usado:

// 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}

Eu costumava escrever código em Asm, C, C++, Pascal, Perl, PHP, Ruby, Python, JavaScript, TypeScript, agora Go. Eu vi tudo isso. Essa experiência me diz que o código deve ser lacônico, fácil de ler e entender .

Eu faço algum projeto de aprendizado de máquina e preciso analisar o arquivo MIDI.
Lá eu preciso analisar o timecode SMPTE. Acho muito difícil usar a maneira idiomática com iota, mas isso não me impede)

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
)

Claro que posso precisar de alguma verificação de tempo de execução com programação defensiva ...

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 tornam a vida dos programadores em alguns casos mais simples. Enums é apenas um instrumento, então se você usá-lo corretamente pode economizar tempo e aumentar a produtividade. Eu acho que não há problemas para implementar isso em Go 2 como em c++, c# ou outras linguagens. Este exemplo é apenas uma piada, mas mostra claramente o problema.

@streeter12 Não vejo como seu exemplo "mostra claramente o problema". Como enums tornariam esse código melhor ou mais seguro?

Existe uma classe C# com implementação da mesma lógica de enum.

 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;
        }
   } 

Com enums de tempo de compilação, posso:

  1. Não tenha verificações de tempo de execução.
  2. Redução significativa da chance de erro por equipe (você não pode passar valor errado em tempo de compilação).
  3. Não contradiz o conceito iota.
  4. Mais fácil de entender a lógica do que ter um nome para constantes (é importante que as constantes representem alguns valores de protocolo de baixo nível).
  5. Você pode tornar o método ToString() analógico para fazer uma representação simples de valores. (CONNECTION_ERROR.NO_INTERNET é melhor que 0x12). Eu sei sobre stringer, mas não há geração de código explícito com enums.
  6. Em alguns idiomas você pode obter array de valores, intervalo e etc.
  7. É simples de entender durante a leitura do código (sem necessidade de cálculos na cabeça).

Afinal, é apenas uma ferramenta para evitar alguns erros humanos comuns e economizar desempenho.

@streeter12 Obrigado por esclarecer o que você quis dizer. A única vantagem sobre as constantes Go aqui é que não se pode introduzir um valor inválido porque o sistema de tipos não aceitará nenhum outro valor além de um dos valores enum. Isso é certamente bom de se ter, mas também tem um preço: não há como estender esse enum fora desse código. A extensão de enumeração externa é uma das principais razões pelas quais em Go decidimos contra as enumerações padrão.

Responda apenas a necessidade simples de fazer algumas extensões, não use enums.
O FE precisa fazer com que a máquina de estado use o padrão de estado em vez de enums.

Enums têm seu próprio escopo. Eu concluo alguns grandes projetos sem qualquer enumeração. Eu acho que é uma decisão de arquitetura terrível estender enum fora do código de definição. Você não tem controle sobre o que seu colega está fazendo e comete alguns erros engraçados)

E você esqueceu que as enumerações do fator humano em muitos casos reduzem significativamente os erros em grandes projetos.

@streeter12 Infelizmente, a realidade é que muitas vezes as enumerações precisam ser estendidas.

@griesemer estendendo um tipo enum/sum cria um tipo separado e às vezes incompatível.

Isso ainda é verdade em Go, embora não haja tipos explícitos para enums/sums. Se você tem um "tipo enum" em um pacote que espera valores em {1, 2, 3} e você passa um 4 do seu "tipo enum estendido", você ainda violou o contrato do "tipo" implícito.

Se você precisar estender um enum/sum, você também precisará criar funções de conversão To/From explícitas que tratam explicitamente dos casos às vezes incompatíveis.

Acho que a desconexão entre esse argumento e as pessoas para esta proposta ou propostas semelhantes como #19412 é que achamos estranho que a troca seja "sempre escrever código de validação básico que o compilador possa manipular" em vez de "às vezes escrever funções de conversão que você provavelmente também vai ter que escrever de qualquer maneira".

Isso não quer dizer que qualquer um dos lados esteja certo ou errado ou que essa seja a única troca a ser considerada, mas eu queria identificar um gargalo na comunicação entre os lados que notei.

Acho que a desconexão entre esse argumento e as pessoas para esta proposta ou propostas semelhantes como #19412 é que achamos estranho que a troca seja "sempre escrever código de validação básico que o compilador possa manipular" em vez de "às vezes escrever funções de conversão que você provavelmente também vai ter que escrever de qualquer maneira".

Muito bem declarado

@jimmyfrasche Não é assim que eu pessoalmente descreveria a troca. Eu diria que é "sempre escrever código de validação básico que o compilador possa manipular" versus "adicionar um conceito totalmente novo ao sistema de tipos que todos que usam Go precisam aprender e entender".

Ou, deixe-me colocar de outra forma. Até onde posso dizer, os únicos recursos significativos ausentes da versão de Go de tipos enumerados é que não há validação de atribuição de constantes não tipadas, não há verificação de conversões explícitas e não há verificação de que todos os valores foram tratados em um trocar. Parece-me que essas características são todas independentes da noção de tipos enumerados. Não devemos deixar que o fato de outras linguagens terem tipos enumerados nos leve à conclusão de que Go também precisa de tipos enumerados. Sim, os tipos enumerados nos dariam esses recursos ausentes. Mas é realmente necessário adicionar um tipo inteiramente novo de tipo para obtê-los? E o aumento na complexidade da linguagem vale os benefícios?

@ianlancetaylor Adicionar complexidade ao idioma é certamente uma coisa válida a considerar, e "porque outro idioma o possui" certamente não é um argumento. Eu, pessoalmente, não acho que os tipos enum valem a pena por conta própria. (Sua generalização, tipos de soma, com certeza marca muitas caixas para mim, no entanto).

Uma maneira geral de um tipo recusar a atribuição seria bom, embora eu não tenha certeza de quão útil isso seria fora dos primitivos.

Não tenho certeza de quão generalizável é o conceito de "verificar todos os valores manipulados em um switch", sem alguma maneira de permitir que o compilador conheça a lista completa de valores legais. Além dos tipos enum e sum, a única coisa em que consigo pensar é algo como os tipos de intervalo de Ada, mas esses não são naturalmente compatíveis com valores zero, a menos que 0 deva estar no intervalo ou código gerado para lidar com deslocamentos sempre que são convertidos ou refletidos sobre. (Outras línguas tiveram famílias de tipos semelhantes, algumas na família pascal, mas Ada é a única que me vem à mente no momento)

De qualquer forma, eu estava me referindo especificamente a:

A única vantagem sobre as constantes Go aqui é que não se pode introduzir um valor inválido porque o sistema de tipos não aceitará nenhum outro valor além de um dos valores enum. Isso é certamente bom de se ter, mas também tem um preço: não há como estender esse enum fora desse código. A extensão de enumeração externa é uma das principais razões pelas quais em Go decidimos contra as enumerações padrão.

e

Infelizmente, a realidade é que muitas vezes as enumerações precisam ser estendidas.

Esse argumento não funciona para mim pelas razões que afirmei.

@jimmyfrasche Entendido; é um problema difícil. É por isso que em Go não tentamos resolvê-lo, mas apenas fornecemos um mecanismo para criar facilmente sequências de constantes sem a necessidade de repetir o valor constante.

(Enviado com atraso - foi feito como resposta a https://github.com/golang/go/issues/19814#issuecomment-349158748)

@griesemer de fato e foi definitivamente a escolha certa para o Go 1, mas vale a pena reavaliar parte do Go 2.

Há o suficiente na linguagem para obter _quase_ tudo o que se deseja dos tipos enum. Requer mais código do que uma definição de tipo, mas um gerador pode lidar com a maior parte dele e permite que você defina o máximo ou o mínimo que se ajuste à situação, em vez de apenas obter os poderes que vêm com um tipo enum.

Esta abordagem https://play.golang.org/p/7ud_3lrGfx te dá tudo, exceto

  1. segurança dentro do pacote de definição
  2. a capacidade de lint um interruptor para a completude

Essa abordagem também pode ser usada para tipos de soma pequenos e simples†, mas é mais difícil de usar, e é por isso que acho que algo como https://github.com/golang/go/issues/19412#issuecomment -323208336 adicionaria a a linguagem e pode ser usado por um gerador de código para criar tipos de enumeração que evitem os problemas 1 e 2.

† veja https://play.golang.org/p/YFffpsvx5e para um esboço de json.Token com esta construção

achamos estranho que a troca seja "sempre escrever código de validação básico que o compilador possa manipular" em vez de "às vezes escrever funções de conversão que você provavelmente também terá que escrever de qualquer maneira".

Para mim - um representante do campo de defensores ferozes do reparo gradual - isso parece ser a troca correta (ish). Honestamente, mesmo que não estejamos falando de reparo gradual, eu consideraria isso um modelo mental melhor.

Por um lado, a enumeração com verificação de tipo só poderá verificar os valores inseridos no código-fonte de qualquer maneira. Se o enum trafegar por uma rede, persistir no disco ou ser trocado entre processos, todas as apostas serão canceladas (e a maioria dos usos propostos de enums se enquadram nessa categoria). Portanto, você não contornará o problema de lidar com incompatibilidades em tempo de execução de qualquer maneira. E não há um comportamento padrão geral de tamanho único para quando você encontra um valor de enumeração inválido. Muitas vezes você pode querer errar. Às vezes você pode querer coagi-lo em um valor padrão. Na maioria das vezes, você deseja preservá-lo e passá-lo, para que não se perca na re-serialização.

É claro que você pode argumentar que ainda deve haver um limite de confiança, onde a validade é verificada e o comportamento necessário é implementado - e tudo dentro desse limite deve poder confiar nesse comportamento. E o modelo mental parece ser que esse limite de confiança deve ser um processo. Porque todo o código em um binário será alterado atomicamente e permanecerá internamente consistente. Mas esse modelo mental é erodido pela ideia de reparo gradual; de repente, o limite de confiança natural torna-se um pacote (ou talvez um repositório) como as unidades às quais você aplica seus reparos atômicos e a unidade que você confia para ser auto-consistente.

E, pessoalmente, acho isso uma unidade muito natural e grande de autoconsistência. Um pacote deve ser grande o suficiente para manter sua semântica, regras e convenções em sua cabeça. É também por isso que as exportações funcionam no nível do pacote, não no nível do tipo e porque as declarações de nível superior têm escopo no nível do pacote, não no nível do programa. Parece bom e economiza o suficiente para mim, para decidir o tratamento correto de valores enum desconhecidos no nível do pacote também. Ter uma função não exportada, que a verifica e mantém o comportamento desejado internamente.

Eu estaria muito mais de acordo com uma proposta de que cada switch precisa de um caso padrão, do que com uma proposta para ter enums verificados por tipo, incluindo verificações de exaustividade.

@Merovius O processo do sistema operacional e o pacote são limites de confiança, como você diz.

As informações provenientes de fora do processo devem ser validadas em sua entrada e desempacotadas em uma representação apropriada para o processo e cuidados apropriados tomados quando isso falhar. Isso nunca vai embora. Eu realmente não vejo nada específico para tipos de soma/enum lá. Você poderia dizer o mesmo sobre structs - às vezes você obtém campos extras ou poucos campos. As estruturas ainda são úteis.

Dito isto, com o tipo enum, é claro que você pode incluir casos específicos para modelar esses erros. Por exemplo

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
}

e com os tipos de soma você pode ir mais longe:

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
}

(O primeiro não é tão útil a menos que seja mantido em uma estrutura com campos específicos para os casos de erro, mas a validade desses campos depende do valor do enum. O tipo de soma cuida disso, pois é essencialmente um struct que só pode ter um campo definido por vez.)

No nível do pacote, você ainda precisa lidar com a validação de alto nível, mas a validação de baixo nível vem com o tipo. Eu diria que reduzir o domínio do tipo ajuda a manter o pacote pequeno e na sua cabeça. Também torna a intenção mais clara para as ferramentas para que seu editor possa escrever todas as linhas case X: e deixar você preencher o código real ou um linter pode ser usado para garantir que todo o código esteja verificando todos os casos (você me convenceu a não ter a exaustividade no compilador anteriormente).

Eu realmente não vejo nada específico para tipos de soma/enum lá. Você poderia dizer o mesmo sobre structs - às vezes você obtém campos extras ou poucos campos. As estruturas ainda são úteis.

Se estamos falando de enums abertos (como os atualmente construídos pelo iota), então, claro. Se estamos falando de enums fechados (que é o que as pessoas geralmente falam, quando falam sobre enums) ou enums com verificações de exaustividade, então eles certamente são especiais. Porque eles não são extensíveis.

A analogia com structs explica isso perfeitamente: A promessa de compatibilidade Go 1 exclui literais de struct sem chave de qualquer promessa - e, portanto, usar literais de struct com chave tem sido uma prática considerada tão fortemente como "melhor", que go vet tem uma verificação para isso. O motivo é exatamente o mesmo: se você estiver usando literais de estrutura sem chave, as estruturas não serão mais extensíveis.

Então sim. Structs são exatamente como enums a esse respeito. E concordamos como comunidade que é preferível usá-los de maneira extensível.

Dito isto, com o tipo enum, é claro que você pode incluir casos específicos para modelar esses erros.

Seu exemplo cobre apenas o limite do processo (falando sobre erros de rede), não o limite do pacote. Como os pacotes se comportarão, se eu adicionar um "InvalidInternalState" (para inventar algo) a FromTheNetwork ? Eu tenho que consertar seus switches antes de compilar novamente? Então não é extensível no modelo de reparo gradual. Eles exigem um caso padrão para compilar em primeiro lugar? Então não parece haver nenhum ponto para enums.

Novamente, ter enumerações abertas é uma questão diferente. Eu estaria a bordo com coisas como

Eu diria que reduzir o domínio do tipo ajuda a manter o pacote pequeno e na sua cabeça. Também torna a intenção mais clara para as ferramentas, para que seu editor possa escrever todas as linhas do caso X: e deixar você preencher o código real ou um linter pode ser usado para garantir que todo o código esteja verificando todos os casos

Mas para isso não precisamos de enums reais, como tipos. Essa ferramenta de linting também pode verificar heuristicamente const -declarations usando iota , onde cada caso é de um determinado tipo e considerar isso "um enum" e executar as verificações desejadas. Eu estaria completamente de acordo com uma ferramenta usando esses "enums por convenção" para ajudar no preenchimento automático ou no linting de que cada switch precisa ter um padrão ou mesmo que todos os casos (conhecidos) devem ser verificados. Eu até não me oporia a adicionar uma palavra-chave enum que se comportasse principalmente assim; ou seja, um enum está aberto (pode receber qualquer valor inteiro), fornece o escopo extra e requer um padrão em qualquer switch (não acho que eles adicionariam o suficiente sobre iota-enums para os custos adicionais, mas pelo menos eles não prejudicariam minha agenda). Se é isso que é proposto - tudo bem. Mas não parece ser o que a maioria dos defensores desta proposta (certamente não o texto inicial) quer dizer.

Podemos discordar sobre a importância de manter possível o reparo gradual e a extensibilidade - por exemplo, muitas pessoas acreditam que o versionamento semântico é uma solução melhor para os problemas que resolve. Mas se você as considera importantes, é perfeitamente válido e razoável ver as enumerações como prejudiciais ou inúteis. E essa era a pergunta que eu estava respondendo: como as pessoas podem razoavelmente fazer a troca de exigir uma verificação em todos os lugares, em vez de tê-la no compilador. Resposta: Valorizando a extensibilidade e evolução das APIs, o que torna essas verificações necessárias no local de uso de qualquer maneira.

De tempos em tempos, os oponentes do enum disseram que não são expansíveis, ainda precisamos de verificações após a serialização/transição, podemos quebrar a compatibilidade etc.

O principal problema que isso não é problemas de enumeração, são seus problemas de desenvolvimento e arquitetura.
Você tenta dar um exemplo onde o uso de enums é ridículo, mas vamos considerar algumas situações com mais detalhes.

Exemplo 1. Sou desenvolvedor de baixo nível e preciso de const para alguns endereços de registradores, estabeleci valores de protocolo de baixo nível etc. Agora em Go eu tenho apenas uma solução: é usar consts sem iota, porque em muitos casos seria feio . Eu posso obter vários blocos de constantes para um pacote e depois de pressionar . eu tenho todas as 20 constantes e se elas tiverem o mesmo tipo e nomes semelhantes, posso cometer erros. Se o projeto for grande, você receberá esse erro. Para evitar isso com programação defensiva, TDD, devemos oferecer código de verificação duplicado (código duplicado = erros/testes duplicados em qualquer caso). Com o uso de transferências não temos problemas semelhantes e os valores nunca serão alterados neste caso (tente encontrar situações em que os endereços dos registros serão alterados em produção :)). Às vezes ainda verificamos se o valor que obtemos de file/net of etc. está no intervalo, mas não há problemas para tornar isso centralizado (consulte c# Enum.TryParsepor exemplo). Com enums eu economizo tempo de desenvolvimento e desempenho neste caso.

Exemplo 2. Estou desenvolvendo um pequeno módulo com lógica de estado/erros. Se eu tornar o enum privado, ninguém nunca sabe sobre este enum, e você pode alterá-lo/estendê-lo sem problemas com todos os benefícios de 1. Se você baseou seu código em alguma lógica privada, algo deu completamente errado em seu desenvolvimento.

Exemplo 3. Estou desenvolvendo um módulo frequentemente alterado e extensível para uma ampla gama de aplicações. É uma solução estranha usar enums ou quaisquer outras constantes para determinar a lógica/interface pública. Se você adicionar um novo número de enumeração na arquitetura cliente-servidor, poderá travar, mas com constantes você poderá obter um estado imprevisível do modelo e até salvá-lo em disco. Eu prefiro travar em vez de estado imprevisível. Isso nos mostra que o problema de retrocompatibilidade / extensão é problema de nossos desenvolvimentos e não de enumerações. Se você entende que enums não são sutable neste caso, apenas não os use. Acho que temos competência suficiente para escolher.

A principal diferença entre consts e compile time enum na minha opinião é que enums tem dois contratos principais.

  1. Contrato de nomenclatura.
  2. Contrato de valores.
    Todos os argumentos a favor e contra este parágrafo foram considerados anteriormente.
    Se você usa programação de contrato você pode facilmente entender os benefícios disso.

Enums quantas outras coisas têm suas desvantagens.
Fe não pode ser alterado sem copabyly de freio. Mas se você conhece O dos princípios SOLID, isso se aplica não apenas ao enum, mas também ao desenvolvimento em geral. Alguém pode dizer, eu faço meu programa com lógica paralela e estruturas mutáveis ​​feio. Vamos proibir estruturas mutáveis? Em vez disso, podemos adicionar estruturas mutáveis/não mutáveis ​​e deixar os desenvolvedores escolherem.

Depois de tudo o que foi dito, quero observar que o Iota também tem suas desvantagens.

  1. Sempre tem o tipo int,
  2. Você precisa calcular valores na cabeça. Você pode perder muito tempo tentando calcular os valores, e verifique se está ok.
    Com enums/const eu posso apenas pressionar F12 e ver todos os valores.
  3. A expressão Iota é uma expressão de código, você também precisa testar isso.
    Em alguns projetos, recusei completamente o uso do iota por esses motivos.

Você tenta dar um exemplo em que o uso de enums é ridículo

Desculpe a franqueza, mas depois desse comentário acho que você não tem muito chão para se firmar aqui.

E eu nem estava fazendo o que você diz - ou seja, dando um exemplo de onde usar enums é ridículo. Peguei um exemplo que deveria mostrar como eles são necessários e ilustrar como eles machucam.

Podemos discordar razoavelmente, mas devemos pelo menos todos argumentar de boa fé.

Exemplo 1

Eu posso dar a você "nomes de registro" como algo que realmente não pode ser alterado, mas em relação aos valores de protocolo, estou convencido de que a posição de tê-los tomando valores arbitrários para extensibilidade e compatibilidade é razoável. Novamente, proto2 -> proto3 continha exatamente essa mudança e o fez a partir da experiência aprendida.

E de qualquer forma, não vejo por que um linter não seria capaz de pegar isso.

eu tenho todas as 20 constantes e se elas tiverem o mesmo tipo e nomes semelhantes, posso cometer erros. Se o projeto for grande, você receberá esse erro.

Se você estiver digitando nomes errados, ter enums fechadas não o ajudará. Somente se você não usar os nomes simbólicos e usar int/string-literals.

Exemplo 2

Pessoalmente, costumo colocar "pacote único" firmemente na linha de "não é um projeto grande". Assim, considero muito menos provável que você esqueça um caso ou altere um local de código, ao estender um enum.

E de qualquer forma, não vejo por que um linter não seria capaz de pegar isso.

Exemplo 3

Esse é o caso de uso mais comum apresentado para enums. Caso em questão: Esta questão específica os usa como justificativa. Outro caso frequentemente mencionado são as syscalls - uma arquitetura cliente-servidor disfarçada. A generalização deste exemplo é "qualquer código onde dois ou mais componentes desenvolvidos independentemente trocam tais valores", que é incrivelmente amplo, cobre a grande maioria dos casos de uso para eles e, sob o modelo de reparo gradual, também qualquer API exportada .

FTR, ainda não estou tentando convencer ninguém de que os enums são prejudiciais (tenho certeza que não vou). Apenas para explicar como cheguei à conclusão de que eles são e por que acho os argumentos a seu favor pouco convincentes.

Sempre tem o tipo int,

iota pode (não necessariamente, mas tanto faz), mas os blocos const não, eles podem ter uma variedade de tipos de constantes - na verdade, um superconjunto das implementações de enum mais comumente propostas.

Você precisa calcular valores na cabeça.

Novamente, você não pode usar isso como um argumento a favor de enums; você pode escrever as constantes assim como em uma declaração de enumeração.

A expressão Iota é uma expressão de código, você também precisa testar isso.

Nem toda expressão precisa ser testada. Se for imediatamente óbvio, o teste é um exagero. Se não for, anote as constantes, você faria isso em um teste de qualquer maneira.

iota não é a maneira atualmente recomendada de fazer enums em Go - const são as declarações. iota serve apenas como uma maneira mais geral de economizar digitação ao escrever declarações const consecutivas ou formuladas.

E sim, as enumerações abertas do Go têm desvantagens, obviamente. Eles foram mencionados acima, extensivamente: Você pode esquecer um caso em um switch, levando a bugs. Eles não têm namespace. Você pode acidentalmente usar uma constante não simbólica que acaba sendo um valor inválido (levando a bugs).
Mas me parece mais produtivo falar sobre essas desvantagens e medi-las em relação às desvantagens de qualquer solução proposta, do que pegar uma solução fixa (tipo enum) e discutir sobre suas compensações específicas para resolver os problemas.

Para mim, a maioria das desvantagens pode ser resolvida pragmaticamente na linguagem atual, com uma ferramenta linter detectando declarações const de um certo tipo e verificando seus usos. O namespace não pode ser resolvido dessa maneira, o que não é ótimo. Mas pode haver uma solução diferente para esse problema, além de enums também.

Eu posso dar a você "nomes de registro" como algo que realmente não pode ser alterado, mas em relação aos valores de protocolo, estou convencido de que a posição de tê-los tomando valores arbitrários para extensibilidade e compatibilidade é razoável. Novamente, proto2 -> proto3 continha exatamente essa mudança e o fez a partir da experiência aprendida.

É por isso que eu disse valores estabelecidos. A base do formato wav Fe não mudou por muitos anos e obteve grande capacidade de volta. Se houver novos valores, você pode usar enums e adicionar alguns valores.

Se você estiver digitando nomes errados, ter enums fechadas não o ajudará. Somente se você não usar os nomes simbólicos e usar int/string-literals.

Sim, isso não me ajuda a fazer bons nomes, mas eles podem ajudar a organizar alguns valores com um nome. Torna o processo de desenvolvimento mais rápido em alguns casos. Ele pode reduzir o número de variantes com autotipagem para uma.

Esse é o caso de uso mais comum apresentado para enums. Caso em questão: Esta questão específica os usa como justificativa. Outro caso frequentemente mencionado são as syscalls - uma arquitetura cliente-servidor disfarçada. A generalização deste exemplo é "qualquer código onde dois ou mais componentes desenvolvidos independentemente trocam tais valores", que é incrivelmente amplo, cobre a grande maioria dos casos de uso para eles e, sob o modelo de reparo gradual, também qualquer API exportada .

Mas usar/não usar constantes/enums não remove o cerne do problema, você ainda precisa pensar em retrocompatibilidade. Eu quero dizer que o problema não está em enums/consts, mas em nossos casos de uso.

Pessoalmente, costumo colocar "pacote único" firmemente na linha de "não é um projeto grande". Assim, considero muito menos provável que você esqueça um caso ou altere um local de código, ao estender um enum.

Nesse caso, você ainda tem benefícios de conversão de nomes e verificação de tempo de compilação,

Nem toda expressão precisa ser testada. Se for imediatamente óbvio, o teste é um exagero. Se não for, anote as constantes, você faria isso em um teste de qualquer maneira.

Claro que eu entendo que nem todas as linhas de código devem ser testadas, mas se você tiver precedentes, você deve testar isso ou reescrever. Eu sei como fazer isso sem iota, mas meu exemplo antigo é apenas uma piada.

Novamente, você não pode usar isso como um argumento a favor de enums; você pode escrever as constantes assim como em uma declaração de enumeração.

Não é argumento para enums.

@Merovius

Se estamos falando de enums fechados (que é o que as pessoas geralmente falam, quando falam sobre enums) ou enums com verificações de exaustividade, então eles certamente são especiais. Porque eles não são extensíveis.

Nem são extensíveis com segurança.

Se você tem

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

e

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

dentro de q é seguro usar D (a menos que a próxima versão de p adicione seu próprio rótulo para Enum(3) ) mas apenas enquanto você nunca passá-lo de volta para p : Você pode pegar o resultado de p.Make e fazer a transição de seu estado para D mas se você chamar p.Take você tem que ter certeza que é não sendo passado q.D E ele tem que garantir que ele receba apenas A , B , C ou você tem um bug. Você pode contornar isso fazendo

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) {...}

Com ou sem um tipo fechado na linguagem você tem todos os problemas de ter um tipo fechado mas sem o compilador cuidar de você.

Seu exemplo cobre apenas o limite do processo (falando sobre erros de rede), não o limite do pacote. Como os pacotes se comportarão, se eu adicionar um "InvalidInternalState" (para inventar algo) a FromTheNetwork? Eu tenho que consertar seus switches antes de compilar novamente? Então não é extensível no modelo de reparo gradual. Eles exigem um caso padrão para compilar em primeiro lugar? Então não parece haver nenhum ponto para enums.

Com apenas tipos de enumeração, você ainda teria que fazer como o acima e definir sua própria versão com o estado extra e as funções de conversão de gravação.

No entanto, os tipos de soma podem ser compostos mesmo quando usados ​​como enums, portanto, você pode "estender" um de maneira natural e segura dessa maneira. Dei um exemplo, mas para ser mais explícito, dado

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

Enum pode ser "estendido" com

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

e desta vez é completamente seguro para uma nova versão de p adicionar D . A única desvantagem é que você tem que mudar duas vezes para chegar ao estado de p.Enum de dentro de um q.Enum , mas é explícito e claro e, como mencionei, seu editor poderia cuspir o esqueleto dos interruptores automaticamente.

Mas para isso não precisamos de enums reais, como tipos. Essa ferramenta de linting também pode verificar heuristicamente as declarações const usando iota, onde cada caso é de um determinado tipo e considerar isso "um enum" e executar as verificações desejadas. Eu estaria completamente de acordo com uma ferramenta usando esses "enums por convenção" para ajudar no preenchimento automático ou no linting de que cada switch precisa ter um padrão ou mesmo que todos os casos (conhecidos) devem ser verificados.

Há dois problemas com isso.

Se você tiver um tipo integral definido com rótulos dados a um subconjunto de seu domínio via const/iota:

Um, pode representar uma enumeração fechada ou aberta. Embora usado principalmente para simular um tipo fechado, também pode ser usado simplesmente para dar nomes a valores comumente usados. Considere uma enumeração aberta para um formato de arquivo imaginário:

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
)

Isso não quer dizer que 0, 1 e 42 são o domínio do tipo Record. O contrato é muito mais sutil e exigiria tipos dependentes para modelar. (Isso definitivamente seria ir longe demais!)

Segundo, poderíamos assumir heuristicamente que um tipo integral definido com rótulos constantes significa que o domínio é restrito. Seria um falso positivo do acima, mas nada é perfeito. Poderíamos usar go/types para extrair esse pseudo-tipo das definições e, em seguida, percorrer e encontrar todos os switches sobre os valores desse tipo e garantir que todos contenham os rótulos necessários. Isso pode ser útil, mas não demonstramos exaustividade neste ponto. Garantimos a cobertura de todos os valores válidos, mas não provamos que nenhum valor inválido foi criado. Fazer isso não é possível. Mesmo que pudéssemos encontrar todas as fontes, sumidouros e transformações de valores e interpretá-los abstratamente para garantir estaticamente que nenhum valor inválido foi criado, ainda não poderíamos dizer nada sobre o valor em tempo de execução, pois o reflect não tem conhecimento do verdadeiro domínio do tipo, pois não está codificado no sistema de tipos.

Há uma alternativa aos tipos enum e sum aqui que contorna isso, embora tenha seus próprios problemas.

Digamos que o tipo literal range m n cria um tipo integral que é no mínimo me no máximo n (Para todo v, m ≤ v ≤ n). Com isso poderíamos limitar o domínio do enum, como

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

Como o tamanho do domínio = o número de rótulos, é possível definir com 100% de confiança se uma instrução switch esgota todas as possibilidades. Para estender esse enum externamente, você precisaria absolutamente criar funções de conversão de tipo para lidar com o mapeamento, mas ainda sustento que você precisa fazer isso de qualquer maneira.

Claro, essa é realmente uma família de tipos surpreendentemente sutil para implementar e não funcionaria tão bem com o resto do Go. Ele também não tem muitos usos fora disso e alguns casos de uso de nicho.

Podemos discordar sobre a importância de manter possível o reparo gradual e a extensibilidade - por exemplo, muitas pessoas acreditam que o versionamento semântico é uma solução melhor para os problemas que resolve. Mas se você as considera importantes, é perfeitamente válido e razoável ver as enumerações como prejudiciais ou inúteis. E essa era a pergunta que eu estava respondendo: como as pessoas podem razoavelmente fazer a troca de exigir uma verificação em todos os lugares, em vez de tê-la no compilador. Resposta: Valorizando a extensibilidade e evolução das APIs, o que torna essas verificações necessárias no local de uso de qualquer maneira.

Para tipos básicos de enumeração, eu concordo. No início desta discussão, eu ficaria simplesmente insatisfeito se eles fossem escolhidos em vez de tipos de soma, mas agora entendo por que eles seriam prejudiciais. Obrigado a você e @griesemer por esclarecer isso para mim.

Para tipos de soma, acho que o que você disse é uma razão válida para não exigir que os switches sejam exaustivos em tempo de compilação. Ainda acho que os tipos fechados têm vários benefícios e que dos três examinados aqui, os tipos soma são os mais flexíveis sem as desvantagens dos outros. Eles permitem que um tipo seja fechado sem inibir a extensibilidade ou o reparo gradual, evitando erros causados ​​por valores ilegais, como qualquer bom tipo.

A principal razão pela qual eu uso golang sobre python e javascript e outras linguagens comuns sem tipo é a segurança de tipo. Eu fiz muito com Java e uma coisa que sinto falta no golang que Java fornece são enums seguros.

Eu discordaria de poder diferenciar os tipos com enums. Se você precisar de ints, apenas use ints, como o Java faz. Se você precisar de enumerações seguras, sugiro a seguinte sintaxe.

type enums enum { foo, bar, baz }

@rudolfschmidt , concordo com você, pode ser assim também:

type DaysOfTheWeek enum {
  Monday
  Tuesday
}

Mas tem uma pequena armadilha - temos que ser capazes de controlar enum nos casos em que temos que validar dados, transformá-los em JSON ou interagir com ou FS.
Se assumirmos cegamente que enum é um conjunto de inteiros sem sinal, podemos terminar com um iota.
Se queremos inovar temos que pensar na conveniência de uso.
Por exemplo, com que facilidade posso validar esse valor dentro do JSON de entrada é um elemento válido de enum?
E se eu lhe disser que o software está mudando?

Digamos que temos uma lista de criptomoedas:

type CryptoCurrency enum {
  BTC
  ETH
  XMR
}

Trocamos dados com vários sistemas de terceiros. Vamos dizer que você tem milhares deles.
Você tem um longo histórico, número de dados armazenados. O tempo passa, digamos, o BitCoin eventualmente morre. Ninguém está usando.
Então você decide retirá-lo da estrutura:

type CryptoCurrency enum {
  ETH
  XMR
}

Isso está causando alteração nos dados. Porque todos os valores de enum foram alterados. Está tudo bem para você. Você pode executar a migração para seus dados. Quanto aos seus parceiros, alguns deles não estão se movendo tão rapidamente, alguns não têm recursos ou simplesmente não podem fazer isso por vários motivos.
Mas você ingere dados deles. Então você vai acabar tendo 2 enums: antigo e novo; e um mapeador de dados usando ambos.
Isso nos diz para dar flexibilidade de definição para enums e habilidades para validar e empacotar/desempacotar esses tipos de dados.

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
}

Temos que pensar na aplicabilidade de enums e casos de uso.

Podemos aprender 2 novas palavras-chave se isso puder nos salvar milhares de linhas de código clichê.

O meio termo é, de fato, ter a capacidade de validar valores de enumeração sem restringi-los no que esses valores podem ser. O tipo de enumeração praticamente permanece o mesmo - é um monte de constantes nomeadas. A variável do tipo enum pode ser igual a qualquer valor do tipo subjacente do enum. O que você adiciona em cima disso é a capacidade de validar um valor para ver, ele contém um valor enum válido ou não. E pode haver outros bônus, como stringificação.

Muitas vezes estou em uma situação em que tenho um protocolo (protobuf ou thrift) com um monte de enums em todo o lugar. Eu tenho que validar cada um deles e, se o valor enum for desconhecido para mim, jogar essa mensagem fora e relatar um erro. Não há outra maneira de lidar com esse tipo de mensagem. Com linguagens em que enum é apenas um monte de constantes, não tenho outra maneira a não ser escrever grandes quantidades de instruções switch verificando todas as combinações possíveis. Essa é uma grande quantidade de código e os erros estão fadados a estar nele. Com algo como C#, posso usar suporte interno para validar enums, o que economiza muito tempo. Algumas implementações de protobuf estão realmente fazendo isso internamente e lançam exceção se for o caso. Sem mencionar como o registro se torna fácil - você obtém a stringificação fora da caixa. É bom que o protobuf gere a implementação do Stringer para você, mas nem tudo no seu código é protobuf.

Mas a capacidade de armazenar qualquer valor é útil em outros casos em que você não deseja jogar mensagens fora, mas fazer algo com elas, mesmo que seja inválido. O cliente geralmente pode jogar a mensagem fora, mas no lado do servidor muitas vezes você precisa armazenar tudo no banco de dados. Jogar conteúdo fora não é uma opção.

Portanto, para mim, há um valor real na capacidade de validar valores de enumeração. Isso me pouparia milhares de linhas de código clichê que não fazem nada além de validação.

Parece muito simples para mim, fornecer essa funcionalidade como uma ferramenta. Parte dele já existe na ferramenta stringer. Se houver uma ferramenta que você chamaria como enumer Foo , que geraria métodos fmt.Stringer para Foo e (digamos) um método Known() bool que verifica se o o valor armazenado está na faixa de valores conhecidos, isso aliviaria seus problemas?

@Merovius na ausência de qualquer outra coisa seria útil. Mas geralmente sou contra o autogen. O único caso em que é útil e funciona é coisas como protobuf, onde você tem um protocolo bastante estável que pode ser compilado uma vez. Usá-lo para enums em geral parece uma muleta para um sistema de tipo simplista. E olhando como Go tem tudo a ver com segurança de tipos, isso parece contra a filosofia da própria linguagem. Em vez de ajudar a linguagem, você começa a desenvolver essa infraestrutura em cima dela, que não faz parte do ecossistema da linguagem. Deixe ferramentas externas para verificação, não para implementar o que está faltando na linguagem.

. Usá-lo para enums em geral parece uma muleta para um sistema de tipo simplista.

Porque é - o sistema de tipos de Go é notoriamente e intencionalmente simplista. Mas essa não era a questão, a questão era se isso aliviaria seus problemas. Além de "eu não gosto disso", eu realmente não vejo como isso não acontece (se você assumir enums abertos de qualquer maneira).

E olhando como Go tem tudo a ver com segurança de tipos, isso parece contra a filosofia da própria linguagem.

Go não é "tudo sobre segurança de tipo". Linguagens como Idris são sobre segurança de tipo. Go é sobre problemas de engenharia em larga escala e, como tal, seu design é impulsionado pelos problemas que está tentando resolver. Por exemplo, seu sistema de tipos permite capturar uma ampla variedade de bugs devido a alterações na API e permite algumas refatorações em larga escala. Mas também é intencionalmente mantido simples, para facilitar o aprendizado, reduzir a divergência de bases de código e aumentar a legibilidade do código de terceiros.

Como tal, se o caso de uso em que você está interessado (open enums) puder ser resolvido sem uma mudança de idioma, por uma ferramenta que gera código de fácil leitura, então isso parece muito alinhado com a filosofia de Go. Em particular, adicionar um novo recurso de linguagem que é um subconjunto da funcionalidade de um existente não parece estar de acordo com o design do Go.

Então, para reiterar: seria útil se você pudesse expandir como o uso de uma ferramenta que gera o clichê com o qual você está preocupado não resolve o problema real - se nada mais, então porque entender isso é necessário para informar o design do recurso de qualquer maneira .

Eu combinei algumas das ideias da discussão, o que você acha disso?

Algumas informações básicas:

  1. Você pode estender um enum como qualquer outro tipo.
  2. Eles são armazenados como uma constante, mas com o nome do tipo como prefixo. Razão: Ao usar iota-enums atuais, você provavelmente escreverá o nome do enum como prefixo de cada constante. Com esse recurso, você pode simplesmente evitá-lo.
  3. Eles são imutáveis ​​e tratados como qualquer outra constante.
  4. Você pode iterar sobre enums. Quando você faz isso, eles se comportam como um mapa. A chave é o nome da enumeração, o valor é o valor da enumeração.
  5. Você pode adicionar métodos a um enum, como faz para qualquer outro tipo.
  6. Cada valor enum tem métodos gerados automaticamente:
  7. Name() retornará o nome da variável enum
  8. Index() retornará o índice enum, que está aumentando automaticamente. Ele começa onde um array começa.

Código:
```vai
pacote principal

//Exemplo A
type Country enum[struct] { //enums podem estender outros tipos (veja exemplo B)
Austria("AT", "Austria", false) //Será acessível como um const, mas com o tipo como
Germany("DE", "Alemanha", true) //prefixo (por exemplo, País.Áustria)

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

}

//Enums podem ter métodos como qualquer outro tipo
func (c País) test() {}

func main(){
println(Country.Austria.CountryName) //Áustria
println(Country.Germany.Code) //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
}

}

//Exemplo B
type Coolness enum[int] {
Muito Legal(10)
Legal(5)
Não Legal(0)
}```

@sinnlosername Acho que enums devem ser algo muito fácil de entender. Combinar algumas das ideias apresentadas na discussão anterior pode não levar necessariamente à melhor ideia para uma enumeração.

Acredito que o seguinte seria simples de entender:

Declaração

type Day enum {
    Monday
    Tuesday
    ...
    Sunday
}

Conversão de String (usando a interface Stringer ):

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

É simples assim. O benefício disso é permitir uma segurança de tipo mais forte ao passar enums.

Exemplo de uso

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

Se eu fosse usar constantes string aqui para representar um Day , IsWeekday diria que qualquer string que não seja "sat" ou "sun" é um dia da semana (ou seja, o que IsWeekday("abc") retornaria?). Em contraste, o domínio da função mostrada acima é restrito, permitindo assim que a função faça mais sentido em relação à sua entrada.

@ljeabmreosn

provavelmente deve ser

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

Desisti de esperar que a equipe golang aprimorasse o idioma de forma necessária. Eu posso recomendar a todos que dêem uma olhada no Rust lang, ele já tem todos os recursos desejados, como enum e genéricos e muito mais.

Estamos em 14 de maio de 2018 e ainda discutimos sobre o suporte enum. Quero dizer, o que diabos? Pessoalmente, estou decepcionado com golang.

Eu posso entender que pode ficar frustrante enquanto espera por um recurso. Mas postar comentários não construtivos como esse não ajuda. Por favor, mantenha seus comentários respeitosos. Consulte https://golang.org/conduct.

Obrigado.

@agnivade tenho que concordar com @rudolfschmidt. GoLang definitivamente não é minha linguagem favorita também por causa dessa falta de recursos, APIs e essa resistência demais para mudar ou aceitar os erros passados ​​pelos criadores do Go. Mas não tenho escolha neste momento porque não fui eu quem tomou a decisão sobre qual idioma escolher para meu último projeto no meu local de trabalho. Então eu tenho que trabalhar com todas as suas deficiências. Mas para ser honesto é como escrever códigos de tortura em GoLang ;-)

Desisti de esperar que a equipe golang aprimorasse o idioma de forma necessária.

  • A palavra necessário não significa "o que eu quero".

Na verdade, os recursos principais de toda linguagem moderna são necessários. O GoLang tem alguns bons recursos, mas não sobreviverá se o projeto permanecer conservador. Recursos como enums ou genéricos não têm desvantagens para quem não gosta deles, mas têm muitas vantagens para quem quer usá-los.

E não me diga "mas vai quer ficar simples". Há uma enorme diferença entre "simples" e "sem recursos reais". Java é muito simples, mas tem muitos recursos que estão faltando. Então, ou os desenvolvedores java são assistentes ou esse argumento é ruim.

Na verdade, os recursos principais de toda linguagem moderna são necessários.

Certo. Esses recursos principais são chamados de completude de Turing. _Tudo_ o resto é uma escolha de design. Há muito espaço entre a completude de Turing e C++ (por exemplo) e nesse espaço você pode encontrar muitas linguagens. A distribuição sugere que não existe um ótimo global.

O GoLang tem alguns bons recursos, mas não sobreviverá se o projeto permanecer conservador.

Possivelmente. Até agora ainda está crescendo. IMO, não estaria ainda crescendo se não fosse conservador. Ambas as nossas opiniões são subjetivas e tecnicamente não valem muito. É a experiência e o gosto dos designers que regem. Não há problema em ter uma opinião diferente, mas isso não garante que os designers a compartilhem.

BTW, se eu imaginar o que seria o Go hoje se 10% dos recursos que as pessoas exigem fossem adotados, eu provavelmente não usaria mais o Go.

Na verdade, você acabou de perder o argumento mais importante da minha resposta. Talvez porque já seja um contra-ataque a algumas das coisas que você disse.

"Recursos como enums ou genéricos não têm desvantagens para quem não gosta deles, mas têm muitas vantagens para quem quer usá-los."

E por que você acha que esse conservadorismo é uma razão para o crescimento de golang? Acho que isso está mais provavelmente relacionado à eficiência do golang e ao grande conjunto de bibliotecas padrão.

Também o java experimentou uma espécie de "crash" ao tentar mudar coisas importantes no java 9, o que provavelmente fez com que muitas pessoas procurassem uma alternativa. Mas olhe para java antes desse crash. Ele estava em constante crescimento porque tinha cada vez mais recursos que facilitavam a vida dos desenvolvedores.

"Recursos como enums ou genéricos não têm desvantagens para quem não gosta deles, mas têm muitas vantagens para quem quer usá-los."

Isso claramente não é verdade. Cada recurso eventualmente chegará ao stdlib e/ou pacotes que eu quero importar. _Todos_ terão que lidar com as novas funcionalidades independente de gostarem ou não.

Até agora ainda está crescendo. IMO, não estaria ainda crescendo se não fosse conservador

Eu não acho que seu crescimento lento (se houver) seja devido ao conservadorismo, mas sim à biblioteca padrão, conjunto já existente de recursos de linguagem, ferramentas. Foi isso que me trouxe aqui. Adicionar recursos de linguagem não mudaria nada para mim a esse respeito.

Se olharmos para C# e Typescript ou mesmo Rust/Swift. Eles estão adicionando novos recursos como loucos. C# ainda está nas principais linguagens flutuando para cima e para baixo. Typescript está crescendo muito rápido. O mesmo para Rust/Swift. Go, por outro lado, explodiu em popularidade em 2009 e 2016. Mas entre isso não estava crescendo nada e realmente perdendo. Go não tem nada a oferecer aos novos desenvolvedores se eles já sabiam disso e não o escolheram antes por algum motivo. Exatamente porque Go está estagnado em seu design. Outras linguagens adicionam recursos não porque não tenham mais nada a fazer, mas porque a base de usuários real exige isso. As pessoas precisam de novos recursos para que sua base de código permaneça relevante em domínios de problemas em constante mudança. Como assíncrono/aguardar. Era necessário resolver um problema real. Não é de surpreender que você possa vê-lo em muitos idiomas agora.

Eventualmente haverá o Go 2 e você pode ter certeza absoluta de que trará muitos novos desenvolvedores. Não porque é novo e brilhante, mas porque novos recursos podem convencer alguém a finalmente mudar ou experimentar. Se o conservadorismo fosse tão importante, teríamos até essas propostas.

Eu não acho que seu crescimento lento (se houver) seja devido ao conservadorismo, mas sim à biblioteca padrão, conjunto já existente de recursos de linguagem, ferramentas. Foi isso que me trouxe aqui.

E esse é o resultado de ser conservador. Se a linguagem quebrar alguma coisa/tudo a cada [meio] ano ou mais, você não faria nada do que você diz que valoriza em Go, porque haverá muito menos pessoas trazendo isso para você.

Adicionar recursos de linguagem não mudaria nada para mim a esse respeito.

Você tem certeza sobre isso? Veja acima.


BTW, você já viu os resultados da Pesquisa de 2017 ?

Se o idioma quebrar algo/tudo a cada [meio] ano ou mais

Então não quebre nada. C# adicionou uma tonelada de recursos e nunca violou a compatibilidade com versões anteriores. Isso também não é uma opção para eles. O mesmo para C++ eu acho. Se Go não pode adicionar recursos sem quebrar algo, então é um problema com Go e, possivelmente, como ele é implementado.

BTW, você já viu os resultados da Pesquisa de 2017?

Meu comentário é baseado em pesquisas de 2017/2018, índice TIOBE e minhas observações gerais sobre o que está acontecendo com vários idiomas.

@cznic
Todo mundo tem que lidar com eles, mas você não precisa usá-los. Se você preferir escrever seu código com enums e mapas iota, ainda poderá fazer isso. E se você não gosta de genéricos, use bibliotecas sem eles. Java prova que é possível ter ambos.

Então não quebre nada.

Boa ideia. No entanto, muitas, se não a maioria das mudanças propostas para a linguagem, _incluindo esta mesma_, são mudanças radicais.

C# adicionou uma tonelada de recursos e nunca violou a compatibilidade com versões anteriores.

Por favor, verifique seus fatos: Visual C# 2010 Breaking Changes . (primeiro resultado de pesquisa na web, só posso adivinhar se é ou não é o único exemplo.)

Meu comentário é baseado em pesquisas de 2017/2018, índice TIOBE e minhas observações gerais sobre o que está acontecendo com vários idiomas.

Bem, como você pode ver o idioma não crescendo enquanto os resultados da pesquisa mostram um crescimento de 70% ano a ano do número de entrevistados?

Como você define "mudanças de ruptura"? Cada linha de código go ainda funcionaria após a adição de enums ou genéricos.

Todo mundo tem que lidar com eles, mas você não precisa usá-los. Se você preferir escrever seu código com enums e mapas iota, ainda poderá fazer isso. E se você não gosta de genéricos, use bibliotecas sem eles. Java prova que é possível ter ambos.

Eu não posso concordar. Uma vez que a linguagem se torne genérica, por exemplo, mesmo que eu não os use, eles serão usados ​​em todos os lugares. Mesmo quando internamente sem alterar a API. O resultado é que sou muito afetado por eles, porque com certeza não há como adicionar genéricos à linguagem sem desacelerar a construção de qualquer programa que os use. Aka "Sem almoço grátis".

Como você define "mudanças de ruptura"? Cada linha de código go ainda funcionaria após a adição de enums ou genéricos.

Claro que não. Este código não compilaria mais com esta proposta:

package foo

var enum = 42

A palavra necessário não significa "o que eu quero".

claro, isso não significa, e eu nunca quis dizer isso. Claro que você pode responder que tais recursos não são necessários, mas então eu posso responder o que é necessário em geral. Nada é necessário e podemos voltar à caneta e ao papel.

Golang afirma ser uma linguagem para grandes equipes. Não tenho certeza se você pode usar golang para desenvolver grandes bases de código. Para isso, você precisa de compilação estática e verificação de tipo para evitar erros de tempo de execução o máximo possível. Como você pode fazer isso sem enums e genéricos? Esses recursos não são nem sofisticados ou agradáveis ​​de se ter, mas absolutamente essenciais para um desenvolvimento sério. Se você não os tiver, acabará usando interfaces{} em todos os lugares. Qual é o sentido de ter tipos de dados se você for forçado a usar interfaces{} em seu código?

Claro, se você não tiver escolha, também fará, mas por que deveria se tiver alternativas como ferrugem que já oferecem todas essas coisas e é ainda mais rápida na execução do que o golang pode ser? Estou realmente me perguntando se go tem um futuro com essa mentalidade de:

A palavra necessário não significa "o que eu quero".

Eu respeito todas as contribuições para o código aberto e se golang é um projeto de hobby, tudo bem como é, mas golang quer ser levado a sério e no momento é mais um brinquedo para alguns desenvolvedores entediados e não vejo vontade de mudar isso.

A API não precisa ser alterada, apenas novas partes da API podem usar genéricos, mas provavelmente sempre existem alternativas sem genéricos na internet.

E ambos, uma compilação um pouco mais lenta e variáveis ​​chamadas "enum" são efeitos mínimos. Na verdade, 99% das pessoas nem vão notar e o outro 1% só precisa adicionar algumas pequenas alterações que são toleráveis. Isso não é comparável, por exemplo, ao quebra-cabeça de java que fu... tudo.

E ambos, uma compilação um pouco mais lenta e variáveis ​​chamadas "enum" são efeitos mínimos. Na verdade, 99% das pessoas nem vão notar e o outro 1% só precisa adicionar algumas pequenas alterações que são toleráveis.

Todos ficariam felizes se alguém pudesse vir com um design e implementação com um desempenho tão maravilhoso. Por favor, contribua para #15292.

No entanto, se este for um jogo chamado "puxar qualquer número a meu favor sem nenhum dado de apoio", desculpe, mas eu não participo.

Você tem algum número para a diferença de velocidade com os genéricos?

E sim, esses números não são apoiados por nenhum dado, porque eles apenas dizem que a probabilidade de ter variáveis ​​chamadas "enum" não é muito alta.

Gostaria de lembrar a todos que há muitas pessoas inscritas nesta edição para a questão específica de se e como enums podem ser adicionados ao Go. As perguntas gerais de "Go é uma boa linguagem?" e "o Go deveria se concentrar mais na entrega de recursos?" provavelmente são melhor discutidos em um fórum diferente.

Você tem algum número para a diferença de velocidade com os genéricos?

Não, por isso não postei nenhum. Eu só postei, que o custo não pode ser zero.

E sim, esses números não são apoiados por nenhum dado, porque eles apenas dizem que a probabilidade de ter variáveis ​​chamadas "enum" não é muito alta.

Isso está misturado. A desaceleração era sobre os genéricos. "enum" era sobre compatibilidade com versões anteriores e seu falso "_Every_ linha de código go ainda funcionaria após adicionar enums ou genéricos". alegar. (enfatiza o meu)

@Merovius Você está certo, agora estou calando a boca.

Trazendo isso de volta aos tipos enum, que é sobre o que se trata este problema, eu entendo completamente o argumento de por que o Go precisa de genéricos, mas sou muito mais instável no argumento de por que o Go precisa de tipos enum. Na verdade, perguntei isso acima em https://github.com/golang/go/issues/19814#issuecomment -290878151 e ainda estou inseguro. Se havia uma boa resposta para isso, eu perdi. Alguém poderia repeti-lo ou apontar para ele? Obrigado.

@ianlancetaylor Não acho que o caso de uso seja complicado, querendo uma maneira segura de tipo para garantir que um valor pertença a um conjunto predefinido de valores, o que não é possível hoje em Go. A única solução é validar manualmente em todos os pontos de entrada possíveis em seu código, incluindo RPCs e chamadas de função, o que é inerentemente não confiável. As outras sutilezas sintáticas para iteração facilitam muitos casos de uso comuns. Se você acha isso valioso ou não é subjetivo, e obviamente nenhum dos argumentos acima foi convincente para os poderes constituídos, então eu essencialmente desisti de que isso seja abordado em um nível de linguagem.

@ianlancetaylor : tudo tem a ver com segurança de tipo. você usa tipos para minimizar o risco de erros de tempo de execução devido a um erro de digitação ou ao uso de tipos incompatíveis. No momento você pode escrever em go

if enumReference == 1

porque no momento enums são apenas números ou outros tipos de dados primitivos.

Esse código não deve ser possível e deve ser evitado. A mesma discussão que você teve na comunidade Java anos atrás, essa é a razão pela qual eles introduziram enums porque eles entenderam a importância.

Você só deve ser capaz de escrever

if enumReference == enumType

você não precisa de muita fantasia para imaginar em quais cenários if enumReference == 1 pode acontecer de forma mais oculta e levar a problemas adicionais que você verá apenas em tempo de execução.

Eu só quero mencionar: Go tem seu potencial, mas é estranho que coisas e conceitos que são comprovados e compreendidos há anos sejam discutidos aqui como você discute novos conceitos ou paradigmas de programação. Se você tiver uma maneira alternativa de garantir a segurança do tipo, talvez haja algo melhor do que enums, mas não o vejo.

Go tem seu potencial, mas é estranho que coisas e conceitos que são comprovados e compreendidos há anos sejam discutidos aqui como você discute novos conceitos ou paradigmas de programação.

Afais, especialmente seguindo as outras discussões sobre genéricos, tipos de soma etc... não se trata tanto de ter, mas de como implementá-lo. O sistema de tipos Java é extremamente extensível e bem especificado. Essa é uma grande diferença.

Em Go, as pessoas estão tentando encontrar maneiras de adicionar recursos à linguagem, sem aumentar a complexidade dos compiladores. Isso geralmente não funciona muito bem e faz com que abandonem essas ideias iniciais.

Embora eu também ache que essas prioridades são bastante absurdas em sua forma e qualidade atuais, sua melhor chance é criar a implementação mais simples possível e menos disruptiva . Qualquer outra coisa não vai te levar mais longe, imo.

@derekperkins @rudolfschmidt Obrigado. Quero deixar claro que, embora C++ tenha tipos de enumeração, os recursos que você está sugerindo não estão em C++. Então não há nada óbvio sobre isso.,

Em geral, se uma variável do tipo enum só puder aceitar valores desse enum, isso seria inútil. Em particular, deve haver uma conversão de um inteiro arbitrário para o tipo enum; caso contrário, você não poderá enviar enumerações por uma conexão de rede. Bem, você pode, mas você tem que escrever um switch com um caso para cada valor enum, o que parece realmente tedioso. Então, ao fazer uma conversão, o compilador gera uma verificação de que o valor é um valor enum válido durante a conversão de tipo? E entra em pânico se o valor for inválido?

Os valores enum precisam ser sequenciais ou podem assumir qualquer valor como em C++?

Em Go, as constantes não são tipadas, portanto, se permitirmos conversões de inteiros para um tipo enum, seria estranho proibir if enumVal == 1 . Mas acho que poderíamos.

Um dos princípios gerais de design do Go é que as pessoas que escrevem Go escrevem código, não tipos. Ainda não vejo nenhuma vantagem nos tipos de enumeração que nos ajudam a escrever código. Eles parecem adicionar um conjunto de restrições de tipo de um tipo que geralmente não temos em Go. Para melhor ou para pior, Go não fornece mecanismos para controlar o valor dos tipos. Portanto, devo dizer que, para mim, o argumento a favor de adicionar enums ao Go ainda não parece convincente.

Vou me repetir, mas sou a favor de manter enums como estão hoje e adicionar recursos em cima deles:

  • tipo enum tem um tipo de valor subjacente e algumas constantes nomeadas associadas a ele
  • o compilador deve permitir a conversão de valor arbitrário para valor enum, desde que seus tipos subjacentes sejam compatíveis. Qualquer valor do tipo int deve ser conversível para qualquer tipo de enumeração inteiro.
  • a conversão que leva a um valor enum inválido é permitida. O tipo enum não deve colocar nenhuma restrição sobre quais valores uma variável pode assumir.

O que ele oferece além disso é:

  • stringificação de valores enum. Pela minha experiência, muito útil para interface do usuário e registro. Se o valor enum for válido, a stringification retornará o nome da constante. Se for inválido, ele retornará a representação de string do valor subjacente. Se -1 não for um valor de enum válido de algum tipo de enum Foo , a stringificação deve retornar apenas -1 .
  • permitir que o desenvolvedor determine se o valor é um valor de enumeração válido em tempo de execução. Muito útil ao trabalhar com qualquer tipo de protocolo. À medida que os protocolos evoluem, novos valores de enumeração podem ser introduzidos que o programa não conhece. Ou pode ser um simples erro. No momento, você precisa garantir que os valores de enumeração sejam estritamente sequenciais (não algo que você sempre possa impor) ou verificar manualmente todos os valores possíveis. Esse tipo de código fica muito grande muito rápido e erros podem acontecer.
  • possivelmente permitir que o desenvolvedor enumere todos os valores possíveis de um tipo enum. Eu vi pessoas pedindo isso aqui, outra linguagem também tem isso, mas eu realmente não me lembro de precisar disso, então não tenho experiência pessoal a favor disso.

Minha justificativa é escrever código e evitar bugs. Todas essas tarefas são tediosas e desnecessárias para o desenvolvedor fazer manualmente ou até mesmo introduzir ferramentas externas que complicam o código e criam scripts. Esses recursos cobrem tudo o que preciso de enums sem complicar e restringi-los excessivamente. Eu não acho que Go precise de algo como enums em Swift ou mesmo Java.


Houve discussões sobre validar em tempo de compilação que a instrução switch cobre todos os valores de enumeração possíveis. Com a minha proposta será inútil. Fazer verificações de exaustão não cobrirá valores de enumeração inválidos, portanto, você ainda precisa ter o caso padrão para lidar com eles. Isso é necessário para dar suporte ao reparo gradual do código. A única coisa que podemos fazer aqui, eu acho, é produzir um aviso se a instrução switch não tiver um caso padrão. Mas isso pode ser feito mesmo sem alterar o idioma.

@ianlancetaylor Acho que seu argumento tem algumas falhas.

Em geral, se uma variável do tipo enum só puder aceitar valores desse enum, isso seria inútil. Em particular, deve haver uma conversão de um inteiro arbitrário para o tipo enum; caso contrário, você não poderá enviar enumerações por uma conexão de rede.

A abstração para o programador é boa; Go fornece muitas abstrações. Por exemplo, o código a seguir não compila:

package main

import "fmt"

const NULL = 0x0

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

mas em C , um programa desse estilo seria compilado. Isso ocorre porque Go é fortemente tipado e C não.

Os índices de enums podem ser armazenados internamente, mas ocultos para o usuário como uma abstração, semelhante aos endereços das variáveis.

@zerkms Sim, essa é uma possibilidade, mas dado o tipo de d , a inferência de tipo deve ser possível; no entanto, o uso qualificado de enums (como no seu exemplo) é um pouco mais fácil de ler.

@ianlancetaylor que é uma versão muito C de enums que você está falando. Tenho certeza que muitas pessoas gostariam disso, mas, imo:

Os valores de enumeração não devem ter propriedades numéricas. Os valores de cada tipo de enumeração devem ser seu próprio universo finito de rótulos discretos, aplicados em tempo de compilação, não relacionados a quaisquer números ou outros tipos de enumeração. A única coisa que você pode fazer com um par desses valores é == ou != . Outras operações podem ser definidas como métodos ou com funções.

A implementação irá compilar esses valores para números inteiros, mas isso não é uma coisa fundamental com qualquer motivo legítimo para ser exposto diretamente ao programador, exceto por inseguro ou reflexão. Pela mesma razão que você não pode fazer bool(0) para obter false .

Se você deseja converter um enum para ou de um número ou qualquer outro tipo, escreva todos os casos e inclua o tratamento de erros apropriado à situação. Se isso for tedioso, você usa um gerador de código como stringer ou pelo menos algo para preencher os casos na instrução switch.

Se você está enviando o valor fora do processo, um int é bom se você está seguindo um padrão bem definido ou sabe que está falando com outra instância do seu programa que foi compilada a partir do código fonte ou precisa fazer coisas cabe no menor espaço possível, mesmo que isso possa causar problemas, mas geralmente nenhum deles é válido e é melhor usar uma representação de string para que o valor não seja afetado pela ordem de origem da definição de tipo. Você não quer que o verde do processo A se torne o azul do processo B porque outra pessoa decidiu que o azul deveria ser adicionado antes do verde para manter as coisas em ordem alfabética na definição: você quer unrecognized color "Blue" .

É uma maneira boa e segura de representar vários estados abstratamente. Deixa o programa definir o que esses estados significam.

(É claro que muitas vezes você deseja associar dados a esses estados e o tipo desses dados varia de estado para estado. . .)

@ljeabmreosn Meu ponto era que, se o Go permitir a conversão de inteiros para tipos enum, seria natural que uma constante sem tipo convertesse automaticamente em um tipo enum. Seu contra-exemplo é diferente, pois Go não permite a conversão de inteiros para tipos de ponteiro.

@jimmyfrasche Se você tiver que escrever uma opção para converter entre inteiros e tipos enum, concordo que funcionaria perfeitamente em Go, mas, francamente, não parece suficientemente útil adicionar à linguagem por si só. Torna-se um caso especial de tipos de soma, para os quais veja #19412.

Há muitas propostas aqui.

Um comentário geral: Para qualquer proposta que não exponha um valor subjacente (por exemplo, um int) que você possa converter de e para um enum, aqui estão algumas perguntas a serem respondidas.

Qual é o valor zero de um tipo enum?

Como você passa de um enum para outro? Suspeito que, para muitas pessoas, os dias da semana sejam um exemplo canônico de enum, mas pode-se razoavelmente querer “incrementar” de quarta a quinta-feira. Eu não gostaria de ter que escrever uma grande instrução switch para isso.

(Além disso, em relação à “stringificação”, a string correta para um dia da semana depende do idioma e da localidade.)

@josharian stringification geralmente significa converter nomes de valores enum em strings automaticamente pelo compilador. Sem localização nem nada. Se você quiser construir algo em cima disso, como localização, então você faz isso por outros meios e outras linguagens fornecem uma linguagem rica e ferramentas de estrutura para fazer isso.

Por exemplo, alguns tipos de C# têm a substituição ToString que também recebe informações de cultura. Ou você pode usar o próprio objeto DateTime e usar seu método ToString que aceita informações de formato e cultura. Mas essas substituições não são padrão, a classe object da qual todos herdam tem apenas ToString() . Muito parecido com a interface do stringer em Go.

Então acho que a localização deve ficar fora dessa proposta e enumerações em geral. Se você quiser implementá-lo, faça-o de outra maneira. Como interface de longarina personalizada, por exemplo.

@josharian Como, em termos de implementação, ainda seria um int e os valores zero são todos os bits zero, o valor zero seria o primeiro valor na ordem de origem. Isso é meio que vazando a intimidade, mas na verdade é muito bom porque você pode escolher o valor zero, decidindo se uma semana começa na segunda ou no domingo, por exemplo. Claro, é menos legal que a ordem dos termos restantes não tenha tanto impacto e que reordenar os valores possa ter impactos não triviais se você alterar o primeiro elemento. Isso não é realmente diferente de const/iota, no entanto.

Re stringification o que @creker disse. Para expandir, no entanto, eu esperaria

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

para imprimir domingo não 0. O rótulo é o valor, não sua representação.

Para ser claro, não estou dizendo que deve ter um método String implícito - apenas que os rótulos sejam armazenados como parte do tipo e acessíveis por reflexão. (Talvez Println chame Label() em um reflect.Value de um enum ou algo assim? Não olhei profundamente como fmt faz seu voodoo.)

Como você passa de um enum para outro? Suspeito que, para muitas pessoas, os dias da semana sejam um exemplo canônico de enum, mas pode-se razoavelmente querer “incrementar” de quarta a quinta-feira. Eu não gostaria de ter que escrever uma grande instrução switch para isso.

Acho que a reflexão ou um grande interruptor é a coisa correta. Padrões comuns podem ser facilmente preenchidos com go generate para criar métodos no tipo ou funções de fábrica desse tipo (e talvez até reconhecidos pelo compilador para reduzi-lo a aritmética na representação).

Não faz sentido para mim supor que todos os enums tenham uma ordem total ou que sejam cíclicos. Dado type failure enum { none; input; file; network } , realmente faz sentido impor que uma entrada inválida seja menor que uma falha de arquivo ou que incrementar uma falha de arquivo resulta em uma falha de rede ou que incrementar uma falha de rede resulta em sucesso?

Supondo que o uso principal seja para valores ordenados cíclicos, outra maneira de lidar com isso seria criar uma nova classe de tipos inteiros parametrizados. Esta é uma sintaxe ruim, mas, para discussão, digamos que é I%N onde I está em um tipo inteiro e N é uma constante inteira. Toda aritmética com um valor desse tipo é implicitamente mod N. Então você pode fazer

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

então sábado + 1 == domingo e dia da semana(456) == segunda-feira. É impossível construir um dia da semana inválido. Pode ser útil fora do const/iota, no entanto.

Para quando você não quer que seja um número y, como @ianlancetaylor apontou, o que eu realmente quero são tipos de soma.

A introdução de um tipo aritmético modular arbitrário é uma sugestão interessante. Então enums podem ser desta forma, o que lhe dá um método String trivial:

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

type Weekday = uint % len(Weekdays)

Combinado com ints de tamanho arbitrário, isso também oferece int128, int256, etc.

Você também pode definir alguns built-ins:

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

O compilador pode provar mais limites do que antes. E as APIs podem fornecer declarações mais precisas por meio de tipos, por exemplo, a função Len64 em math/bits agora pode retornar uint % 64 .

Ao trabalhar em uma porta RISC-V, eu queria um tipo uint12 , já que meus componentes de codificação de instruções são de 12 bits; que poderia ter sido uint % (1<<12) . Muita manipulação de bits, particularmente protocolos, podem se beneficiar disso.

As desvantagens são significativas, é claro. Go tende a favorecer o código sobre os tipos, e isso é muito pesado. Operações como + e - podem de repente se tornar tão caras quanto %. Sem algum tipo de parametricidade de tipo, você provavelmente terá que converter para o canônico uint8 , uint16 , etc. para interoperar com quase qualquer função de biblioteca, e a conversão de volta pode ocultar limites falhas (a menos que tenhamos uma maneira de fazer a conversão panic-on-out-of-range, que introduz sua própria complexidade). E eu posso ver isso sendo usado em excesso, por exemplo, usando uint % 1000 para códigos de status HTTP.

Uma ideia interessante, no entanto, no entanto. :)


Outras respostas menores:

Isso é meio que vazando a intimidade

Isso me faz pensar que eles realmente são ints. :)

Padrões comuns podem ser facilmente preenchidos com go generate

Se você tiver que gerar código com enums de qualquer maneira, parece-me que você também pode gerar funções de String e verificações de limites e afins e fazer enums com geração de código em vez do peso do suporte ao idioma.

Não faz sentido para mim supor que todos os enums tenham uma ordem total ou que sejam cíclicos.

Justo. Isso me faz pensar que ter um punhado de casos de uso concretos ajudaria a esclarecer exatamente o que queremos das enumerações. Eu meio que suspeito que não haverá um conjunto claro de requisitos, e que emular enums usando outras construções de linguagem (ou seja, o status quo) acabará fazendo mais sentido. Mas isso é apenas uma hipótese.

Re stringification o que @creker disse.

Justo. Mas eu me pergunto quantos casos acabam sendo como dias da semana. Qualquer coisa voltada para o usuário, com certeza. E a stringificação parece ser uma das principais solicitações de enums.

@josharian Enums que são realmente ints provavelmente precisariam de um mecanismo semelhante. Caso contrário, o que é enum { A; B; C}(42) ?

Você pode dizer que é um erro do compilador, mas isso não funciona em código mais complicado, pois você pode converter de e para ints em tempo de execução.

É A ou um pânico em tempo de execução. Em ambos os casos, você está adicionando um tipo integral com um domínio limitado. Se for um pânico em tempo de execução, você está adicionando um tipo integral que entra em pânico no estouro quando os outros se fecham. Se for A, você adicionou uint%N com alguma cerimônia.

A outra opção é não deixar ser A, B ou C, mas é o que temos hoje com const/iota para não haver ganhos.

Todas as razões pelas quais você diz que int%N não entrará no idioma parecem se aplicar igualmente a enums que são meio ints. (Embora eu não ficaria com raiva se algo como eles fossem incluídos).

Tirar a intimidade remove esse enigma. Ele requer geração de código para casos em que você deseja adicionar de volta parte dessa int-iness, mas também oferece a opção de não fazer isso, o que permite controlar a quantidade de int-iness a ser introduzida e de que tipo: você pode adicionar nenhum " next", um método next cíclico ou um método next que retorna um erro se você cair da borda. (Você também não acaba com coisas como Monday*Sunday - Thursday sendo legais). A rigidez extra o torna um material de construção mais maleável. Um sindicato discriminado modela bem a variedade não-int-y: pick { A, B, C struct{} } , entre outras coisas.

Os principais benefícios de ter informações como essa no idioma são que

  1. Valores ilegais são ilegais.
  2. a informação está disponível para refletir e ir/tipos permitindo que os programas atuem sobre ela sem a necessidade de fazer suposições ou anotações (que não estão disponíveis para refletir, atualmente).

Os principais benefícios de ter informações como esta no idioma são que: Valores ilegais são ilegais.

Acho importante enfatizar que nem todos veem isso como um benefício. Eu certamente não. Muitas vezes torna mais fácil consumir valores, muitas vezes torna mais difícil produzi-los. O que você pesa mais parece, até agora, até a preferência pessoal. Assim, também é a questão de saber se é um benefício líquido geral.

Também não vejo sentido em proibir valores ilegais. Se você já tem meios de verificar a validade por conta própria (como na minha proposta acima), que benefício essa limitação traz? Para mim, isso só complica as coisas. Em meus aplicativos, as enumerações para a maioria dos casos podem conter valores inválidos/desconhecidos e você teve que contornar isso dependendo do aplicativo - jogue fora completamente, faça o downgrade para algum padrão ou salve como está.

Imagino que enums estritos que não permitem valores inválidos podem ser úteis em casos muito limitados em que seu aplicativo está isolado do mundo externo e não tem como receber uma entrada inválida. Como enumerações internas que só você pode ver e usar.

const com iota não é seguro em tempo de compilação, a verificação seria atrasada para o tempo de execução e a verificação segura não está no nível do tipo. Então eu acho que o iota não pode substituir o enum literalmente, eu prefiro o enum porque é mais poderoso.

Valores ilegais são ilegais.
Acho importante enfatizar que nem todos veem isso como um benefício.

Não entendo essa lógica. Tipos são conjuntos de valores. Você não pode atribuir um tipo a uma variável cujo valor não esteja nesse tipo. Estou entendendo mal alguma coisa?

PS: Concordo que enums são um caso especial de tipos de soma e esse problema deve ter precedência sobre este.

Deixe-me reformular/ser mais preciso: nem todo mundo vê como um benefício o fechamento dos enums.

Se você quiser ser rigoroso dessa maneira, então a) "Valores ilegais são ilegais" é uma tautologia eb) portanto, não pode ser contado como um benefício. Com enums baseados em const, em sua interpretação, valores ilegais também são ilegais. O tipo apenas permite muito mais valores.

Se enums são ints e qualquer int é válido (do ponto de vista do sistema de tipos), o único ganho é que os valores nomeados do tipo estão refletidos.

Isso é basicamente apenas const/iota, mas você não precisa executar stringer, pois o pacote fmt pode obter os nomes usando reflexão. (Você ainda teria que executar stringer se quisesse que as strings fossem diferentes dos nomes na fonte).

A stringificação @jimmyfrasche é apenas um bom bônus. O principal recurso para mim, como você pode ler na minha proposta acima, é a capacidade de verificar se o valor fornecido é um valor válido do tipo de enumeração fornecido em tempo de execução.

Por exemplo, dado algo assim

type Foo enum {
    Val1 = 1
    Val2 = 2
}

E método de reflexão como

func IsValidEnum(v {}interface) bool

Nós poderíamos fazer algo assim

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

Para um exemplo do mundo real, você pode ver enums em C# que, na minha opinião, capturaram perfeitamente esse meio-termo em vez de seguir cegamente o que o Java fez. Para verificar a validade em C#, você usa o método estático Enum.IsDefined .

@crecker A única diferença entre isso e const/iota é o
informações armazenadas em refletir. Isso não é muito ganho para um todo
novo tipo de tipo.

Ideia meio maluca:

Armazene os nomes e valores de todas as consts declaradas no mesmo pacote
como seu tipo definido de uma maneira que o reflect pode chegar. Seria
estranho destacar essa classe estreita de uso const, no entanto.

A principal característica para mim, como você pode ler na minha proposta acima

IMO isso ilustra uma das principais coisas que arrastam esta discussão: A falta de clareza do que é o conjunto de "principais recursos". Todo mundo parece ter idéias ligeiramente diferentes sobre isso.
Pessoalmente, ainda gosto do formato dos relatos de experiência para descobrir esse conjunto. Existe até um na lista (embora, pessoalmente, eu ainda comente o fato de que a seção "O que deu errado" menciona apenas o que poderia dar errado, não o que realmente deu ). Talvez adicionar alguns, ilustrando onde a falta de verificação de tipo leva a interrupções/bugs ou, por exemplo, falha em fazer refatorações em larga escala, seria útil.

@jimmyfrasche, mas isso resolve um grande problema em muitos aplicativos - validar dados de entrada. Sem qualquer ajuda do sistema de tipos, você precisa fazer isso manualmente e isso não é algo que você possa fazer em algumas linhas de código. Ter alguma forma de validação assistida por tipo resolveria isso. Adicionar stringificação em cima disso simplificaria o registro, pois você teria nomes formatados corretamente e não os valores de tipo subjacentes.

Por outro lado, tornar as enumerações restritas limitaria severamente os possíveis casos de uso. Agora você não pode usá-los em protocolos facilmente, por exemplo. Para preservar até mesmo valores inválidos, você teria que descartar enums e usar tipos de valor simples, possivelmente convertendo-os em enums posteriormente, se necessário. Em alguns casos, você pode descartar o valor inválido e gerar um erro. Em outros, você pode fazer o downgrade para algum valor padrão. De qualquer forma, você está lutando com restrições do seu tipo de sistema em vez de ajudá-lo a evitar erros.

Basta ver o que o protobuf para Java deve gerar para contornar as enumerações Java.

@Merovius em relação à validação, acho que já cobri isso várias vezes. Não sei o que mais poderia ser adicionado - sem validação, você precisa escrever grandes quantidades de código de copiar e colar para validar sua entrada. O problema é óbvio, assim como a solução proposta pode ajudar nisso. Eu não trabalho em algum aplicativo de grande escala que todo mundo conhece, mas erros nesse código de validação me morderam o suficiente em vários idiomas com o mesmo conceito de enums que eu quero ver algo feito sobre isso.

Por outro lado, não vejo (desculpe se esqueci alguma coisa) nenhum argumento a favor da implementação de enums que não permitem valores inválidos. É legal e legal em teoria, mas não vejo isso me ajudando em aplicações reais.

Não há muitos recursos que as pessoas desejam de enums. Stringification, validação, estrito/solto em termos de valores inválidos, enumeração - é praticamente isso pelo que posso ver. Todo mundo (incluindo eu, é claro) apenas os embaralha neste momento. estrito/solto parece ser o principal ponto de discórdia por causa de sua natureza conflitante. Eu não acho que todos vão concordar com um ou outro. Talvez a solução seja incorporar os dois de alguma forma e deixar o programador escolher, mas não conheço nenhuma linguagem que tenha isso para ver como poderia funcionar no mundo real.

@crecker minha sugestão para armazenar as consts nos dados de exportação acima
circunstâncias permitiriam que o tipo de coisas que você está pedindo
sem a introdução de um novo tipo de tipo.

Não tenho certeza se essa é a maneira idiomática e também sou bastante novo no idioma, mas o seguinte funciona e é conciso

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"
}

Vantagens :

Desvantagens :

Nós realmente precisamos de enums?

O que impede alguém de fazer algo assim:

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

O consumidor de tal variável pode ou não perceber isso com a verificação adequada dos valores esperados (supondo que não haja queda ruim por meio de suposições em instruções switch, como qualquer coisa que não seja sábado ou domingo é um dia da semana, por exemplo), mas não seria até o tempo de execução. Eu acho que alguém preferiria que esse tipo de erro fosse detectado em tempo de compilação, não em tempo de execução.

@bpkroth
Por ter Day em seu próprio pacote e expor apenas campos e métodos selecionados, não posso criar novos valores do tipo Day fora de package day
Além disso, desta forma não posso passar estruturas anônimas para getTask

./day/day.go

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)
    }
}

Nunca vi em toda a minha vida outra linguagem que insista em não adicionar os recursos mais simples e úteis como enums, operadores ternários, compilação com variáveis ​​não utilizadas, tipos de soma, genéricos, parâmetros padrão, etc...

Golang é um experimento social para ver o quão estúpidos os desenvolvedores podem ser?

@gh67uyyghj Alguém marcou seu comentário como fora do tópico! e eu acho que alguém vai fazer o mesmo com a minha resposta. mas acho que a resposta para sua pergunta é SIM. No GoLang, ser sem recursos significa ser cheio de recursos, então qualquer coisa que o GoLang não tenha é na verdade um recurso que o GoLang possui e que outras linguagens de programação não possuem!

@L-oris Esta é uma maneira muito interessante de implementar enums com tipos. Mas parece estranho, e ter uma palavra-chave enum (que necessariamente complica um pouco mais a linguagem) tornaria mais fácil:

  • escrever
  • leitura
  • razão sobre

No seu exemplo (o que é ótimo porque funciona hoje) ter enums (de alguma forma) implica a necessidade de:

  • Criar um tipo de estrutura
  • Crie um método
  • Crie variáveis ​​(nem mesmo constantes, embora um usuário da biblioteca não possa alterar esses valores)

Isso leva mais tempo (embora não muito mais) para ler, escrever e raciocinar (discernir que representa e deve ser usado como enumerações).

Portanto, acho que a proposta de sintaxe atinge a nota certa em termos de simplicidade e valor agregado à linguagem.

Obrigado @andradei
Sim, é uma solução alternativa, mas sinto que o objetivo da linguagem é mantê-la pequena e simples
Também poderíamos argumentar que perdemos aulas, mas vamos passar para Java :)

Prefiro me concentrar nas propostas do Go 2, melhor tratamento de erros, por exemplo. me forneceria muito mais valor do que esses enums

Voltando aos seus pontos:

  • não é muito clichê; na pior das hipóteses, podemos ter alguns geradores (mas, é realmente tanto código?)
  • quanta “simplicidade” estamos alcançando ao adicionar uma nova palavra-chave e todo o conjunto específico de comportamentos que ela provavelmente terá?
  • ser um pouco criativo com métodos também pode adicionar recursos interessantes a esses enums
  • para legibilidade, trata-se mais de se acostumar com isso; talvez adicionar um comentário em cima dele, ou prefixar suas variáveis
package day

// Day Enum
type Day struct {
    value string
}

@L-oris eu vejo. Também estou animado com as propostas do Go 2. Eu diria que os genéricos aumentarão a complexidade da linguagem mais do que as enumerações. Mas para manter seus pontos:

  • Não é muito clichê de fato
  • Teríamos que verificar o quão conhecido é o conceito de enum para ter certeza, eu diria que a maioria das pessoas sabe o que é (mas não posso provar isso). A complexidade da linguagem teria um bom "preço" para pagar seus benefícios.
  • É verdade, não ter enums são problemas que só surgiram para mim ao verificar o código protobuf generetad e ao tentar fazer um modelo de banco de dados que imita um enum, por exemplo.
  • Isso também é verdade.

Tenho pensado muito nessa proposta e vejo o grande valor que a simplicidade tem na produtividade e por que você se inclina a mantê-la, a menos que uma mudança seja claramente necessária. Enums também podem mudar o idioma tão drasticamente que não é mais Go, e avaliar os prós / contras disso parece que levará muito tempo. Então, tenho pensado que soluções simples como a sua, onde o código ainda é fácil de ler, são uma boa solução pelo menos por enquanto.

Pessoal, quero muito esse recurso para o futuro!. Ponteiros e a maneira de definir " enums " em _hoje em dia_ não se dão muito bem. Por exemplo: https://play.golang.org/p/A7rjgAMjfCx

Minha proposta para enum está seguindo. Devemos considerar isso como um novo tipo. Por exemplo, gostaria de usar o tipo enum com estrutura arbitrária e a seguinte implementação:

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

É compreensível como organizar essa estrutura e como trabalhar e como mudar para string e assim por diante.
E é claro que se eu pudesse substituir a função "Next" seria ótimo.

Para isso, Go teria que suportar estruturas imutáveis ​​profundas primeiro. Sem tipos imutáveis, posso imaginar que você poderia fazer isso com enums para ter a mesma coisa:

type Status enum {
  Started
  Stopped
}

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

acho que deveria ser mais simples

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

Proposta para Go2

Logicamente enums devem fornecer interface de tipo.
Afirmei que as enumerações anteriores deveriam ser separadas.
São constantes explicitamente nomeadas vinculadas a um namespace específico.

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)
  }
}

Enums são extensões de tipo, "contêineres constantes".

Para os amantes do tipo

Alternativas de sintaxe para quem quiser ver como tipo

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

Mas também podemos evitar essas declarações explícitas de nível superior

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

O exemplo de validação permanece o mesmo.

mas no caso

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
}

Como é Status1.Started == Status2.Started ?
sobre Marshaling?

Se eu mudar de posição?

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

Eu concordo com @Goodwine sobre tipos imutáveis.

Marshaling é uma questão interessante.
Isso tudo depende de como vamos tratar o valor subjacente. Se formos usar valores reais, portanto Status1.Started seria igual a Status2.Started .
Se formos com a interpretação simbólica, esses seriam considerados valores diferentes.

A inserção de algo causará uma mudança nos valores (exatamente da mesma maneira que acontece com iota ).
Para evitar isso, o desenvolvedor precisa especificar valores junto com as declarações.

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

Isso é coisa óbvia.
Se quisermos evitar esses problemas, temos que fornecer uma saída previsível do compilador com base na interpretação léxica dos valores de enumeração. Eu assumo a maneira mais simples - construindo uma tabela de hash ou aderindo a nomes simbólicos (strings), a menos que a conversão de tipo personalizado seja definida.

Eu gosto de como o Rust é implementado Enums.

Padrão sem tipo especificado

enum IpAddr {
    V4,
    V6,
}

Tipo personalizado

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

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

Tipos complexos

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

Com certeza, mesmo ter enums simples como em C# que são armazenados como tipos integrais seria ótimo.

O acima vai além de enum s, esses são _uniões discriminadas_, que são realmente mais poderosas, especialmente com _pattern matching_, que poderia ser uma pequena extensão para switch , algo como:

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"
}

Não preciso de verificações de enums em tempo de compilação, pois isso pode ser perigoso, conforme mencionado

O que eu precisava várias vezes teria sido iterar sobre todas as constantes de um determinado tipo:

  • seja para validação (se tivermos certeza de que queremos apenas aceitar isso ou simplesmente ignorar opções desconhecidas)

    • ou para uma lista de possíveis constantes (pense em dropdowns).

Poderíamos fazer a validação com iota e especificar o final da lista. No entanto, usar iota para qualquer outra coisa além de apenas dentro do código, seria bastante perigoso porque as coisas quebrarão inserindo uma constante na linha errada (eu sei que precisamos estar cientes de onde colocamos as coisas na programação, mas um bug como esse é muito mais difícil de encontrar do que outras coisas). Além disso, não temos descrição do que a constante realmente representa quando é um número. Isso leva ao próximo ponto:

Um bom extra seria especificar nomes de stringify para ele.

O que impede alguém de fazer algo assim:

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

O consumidor de tal variável pode ou não perceber isso com a verificação adequada dos valores esperados (supondo que não haja queda ruim por meio de suposições em instruções switch, como qualquer coisa que não seja sábado ou domingo é um dia da semana, por exemplo), mas não seria até o tempo de execução. Eu acho que alguém preferiria que esse tipo de erro fosse detectado em tempo de compilação, não em tempo de execução.

@L-oris Então, o que dizer disso:

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
}

O que queremos NÃO é RUNTIME SILENCE & Weird BUG causado pelo [retorno "nada a fazer"], mas um RELATÓRIO DE ERRO em tempo de compilação / tempo de codificação !
COMPREENDO?

  1. enum é de fato novo tipo, que é o que type State string faz, não há necessidade idiomática de introduzir uma nova palavra-chave. Go não é sobre economizar espaço em seu código-fonte, é sobre legibilidade, clareza de propósito.

  2. A falta de segurança de tipo, confundindo os novos tipos baseados em string - ou int para strings/ints reais é o principal obstáculo. Todas as cláusulas enum são declaradas como const , o que cria um conjunto de valores conhecidos que o compilador pode verificar.

  3. Stringer interface é o idioma para representar qualquer tipo como texto legível por humanos. Sem personalização, type ContextKey string enums este é o valor da string, e para iota -enums gerados é o inteiro, bem como códigos XHR ReadyState (0 - não enviado, 4 - concluído) em JavaScript.

    Em vez disso, o problema está na falibilidade da implementação personalizada func (k ContextKey) String() string , que geralmente é feita usando um switch que deve conter todas as constantes de cláusula enum conhecidas.

  4. Em uma linguagem como Swift, existe a noção de _uma troca exaustiva_. Essa é uma boa abordagem tanto para a verificação de tipo em um conjunto de const se para a construção de uma maneira idiomática de invocar essa verificação. A função String() , sendo uma necessidade comum, é um ótimo caso para implementação.

Proposta

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
}

Valores PS Associated em enums Swift são um dos meus truques favoritos. Em Go não há lugar para eles. Se você quiser ter um valor ao lado de seus dados de enumeração - use um struct fortemente tipado envolvendo os dois.

Alguns meses atrás, escrevi uma prova de conceito para um linter que verifica se os tipos enumerados são tratados adequadamente. https://github.com/loov/enumcheck

Atualmente ele usa comentários para marcar coisas como enumerações:

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")
    }
}

Fiquei preso em descobrir como lidar com todas as conversões implícitas, mas funciona decentemente para casos básicos.

Observe que, atualmente, ainda é um trabalho em andamento, portanto, as coisas podem mudar. por exemplo, em vez de comentários, poderia usar algum pacote stub para anotar os tipos, mas os comentários são bons o suficiente no momento.

A implementação atual de enums no Go1 é a implementação de enum mais estranha e óbvia em qualquer linguagem que eu conheço. Até mesmo C os implementa mais bem. A coisa iota parece um hack. E o que diabos significa iota de qualquer maneira? Como devo memorizar essa palavra-chave? Go é suposto ser fácil de aprender. Mas isso é apenas peculiar.

@pofl :
Embora eu concorde que as enumerações do Go sejam bem estranhas, iota é na verdade apenas uma palavra normal em inglês:

iota
_substantivo_

  1. uma quantidade muito pequena; mínimo; branco.
  2. a nona letra do alfabeto grego (I, ι).
  3. o som da vogal representado por esta letra.

Presumivelmente, eles estavam indo para a definição um em termos de uso na linguagem.

Em uma nota lateral em resposta a um comentário mais antigo aqui:
Embora eu também goste de uniões discriminadas em Go, sinto que elas deveriam ser separadas das enumerações reais. Com a forma como os genéricos estão indo atualmente, você pode realmente obter algo muito semelhante a uniões discriminadas por meio de listas de tipos em interfaces. Veja #41716.

O uso de iota em Go é vagamente baseado em seu uso em APL. Citando https://en.wikipedia.org/wiki/Iota :

Em algumas linguagens de programação (por exemplo, A+, APL, C++[6], Go[7]), iota (como o símbolo minúsculo ⍳ ou o identificador iota) é usado para representar e gerar uma matriz de inteiros consecutivos. Por exemplo, em APL ⍳4 dá 1 2 3 4.

Esta página foi útil?
0 / 5 - 0 avaliações