Go: proposition : spec : ajouter la prise en charge des énumérations typées

Créé le 1 avr. 2017  ·  180Commentaires  ·  Source: golang/go

Je voudrais proposer que enum soit ajouté à Go en tant que type spécial de type . Les exemples ci-dessous sont empruntés à l'exemple protobuf.

Énumérations dans Go aujourd'hui

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
}

À quoi cela pourrait ressembler avec le support linguistique

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

Le modèle est suffisamment courant pour que je pense qu'il justifie une casse spéciale, et je pense qu'il rend le code plus lisible. Au niveau de la mise en œuvre, j'imagine que la majorité des cas peuvent être vérifiés au moment de la compilation, dont certains se produisent déjà aujourd'hui, tandis que d'autres sont presque impossibles ou nécessitent des compromis importants.

  • Sécurité pour les types exportés : rien n'empêche quelqu'un de faire SearchRequest(99) ou SearchRequest("MOBILEAPP") . Les solutions de contournement actuelles incluent la création d'un type non exporté avec des options, mais cela rend souvent le code résultant plus difficile à utiliser/documenter.
  • Sécurité d'exécution : tout comme protobuf va vérifier la validité lors du désassemblage, cela fournit une validation à l'échelle du langage, chaque fois qu'une énumération est instanciée.
  • Outils / Documentation : de nombreux packages mettent aujourd'hui des options valides dans les commentaires de champ, mais tout le monde ne le fait pas et il n'y a aucune garantie que les commentaires ne soient pas obsolètes.

Choses à considérer

  • Nil : en implémentant enum au-dessus du système de type, je ne pense pas que cela devrait nécessiter une casse spéciale. Si quelqu'un veut que nil soit valide, alors l'énumération doit être définie comme un pointeur.
  • Valeur par défaut / affectations d'exécution : c'est l'une des décisions les plus difficiles à prendre. Que se passe-t-il si la valeur par défaut Go n'est pas définie comme une énumération valide ? L'analyse statique peut atténuer une partie de cela au moment de la compilation, mais il faudrait un moyen de gérer les entrées extérieures.

Je n'ai pas d'avis tranché sur la syntaxe. Je crois que cela pourrait être bien fait et aurait un impact positif sur l'écosystème.

Go2 LanguageChange NeedsInvestigation Proposal

Commentaire le plus utile

@ md2perpe ce n'est pas des énumérations.

  1. Ils ne peuvent pas être énumérés, itérés.
  2. Ils n'ont pas de représentation de chaîne utile.
  3. Ils n'ont pas d'identité :

``` allez
paquet principal

importer (
"fmt"
)

fonction principale() {
tapez SearchRequest int
constante (
Demande de recherche universelle = iota
la toile
)

const (
    Another SearchRequest = iota
    Foo
)

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

}
````

Je suis totalement d'accord avec @derekperkins sur le fait que Go a besoin d'un enum en tant que citoyen de première classe. À quoi cela ressemblerait, je ne suis pas sûr, mais je soupçonne que cela pourrait être fait sans casser la maison de verre Go 1.

Tous les 180 commentaires

@derekparker il y a une discussion pour faire une proposition Go2 en #19412

J'ai lu cela plus tôt dans la journée, mais cela semblait plus axé sur les types valides, où cela se concentre sur les valeurs de type valides. Il s'agit peut-être d'un sous-ensemble de cette proposition, mais il s'agit également d'un changement moins profond du système de type qui pourrait être mis en Go aujourd'hui.

Les énumérations sont un cas particulier de types somme où tous les types sont identiques et une valeur est associée à chacun par une méthode. Plus à taper, sûrement, mais même effet. Quoi qu'il en soit, ce serait l'un ou l'autre, les types de somme couvrent plus de terrain, et même les types de somme sont peu probables. Rien ne se passe jusqu'à Go2 à cause de l'accord de compatibilité Go1, en tout cas, puisque ces propositions nécessiteraient, à tout le moins, un nouveau mot-clé, si l'un d'entre eux était accepté

Assez juste, mais aucune de ces propositions ne rompt l'accord de compatibilité. Il y avait une opinion exprimée que les types de somme étaient "trop ​​gros" pour être ajoutés à Go1. Si tel est le cas, alors cette proposition est un terrain d'entente précieux qui pourrait être un tremplin vers les types de somme complète dans Go2.

Ils nécessitent tous les deux un nouveau mot-clé qui casserait le code Go1 valide en l'utilisant comme identifiant

Je pense que cela pourrait être contourné

Une nouvelle fonctionnalité de langage nécessite des cas d'utilisation convaincants. Toutes les fonctionnalités du langage sont utiles, sinon personne ne les proposerait ; la question est : sont-ils suffisamment utiles pour justifier de compliquer le langage et d'obliger tout le monde à apprendre les nouveaux concepts ? Quels sont les cas d'utilisation convaincants ici ? Comment les gens les utiliseront-ils ? Par exemple, les gens s'attendraient-ils à pouvoir itérer sur l'ensemble des valeurs d'énumération valides, et si oui, comment feraient-ils cela ? Cette proposition fait-elle plus que vous permettre d'éviter d'ajouter des cas par défaut à certains commutateurs ?

Voici la manière idiomatique d'écrire des énumérations dans le Go actuel :

type SearchRequest int

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

Cela a l'avantage qu'il est facile de créer des drapeaux qui peuvent être OR:ed (en utilisant l'opérateur | ):

type SearchRequest int

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

Je ne vois pas que l'introduction d'un mot-clé enum le rendrait beaucoup plus court.

@ md2perpe ce n'est pas des énumérations.

  1. Ils ne peuvent pas être énumérés, itérés.
  2. Ils n'ont pas de représentation de chaîne utile.
  3. Ils n'ont pas d'identité :

``` allez
paquet principal

importer (
"fmt"
)

fonction principale() {
tapez SearchRequest int
constante (
Demande de recherche universelle = iota
la toile
)

const (
    Another SearchRequest = iota
    Foo
)

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

}
````

Je suis totalement d'accord avec @derekperkins sur le fait que Go a besoin d'un enum en tant que citoyen de première classe. À quoi cela ressemblerait, je ne suis pas sûr, mais je soupçonne que cela pourrait être fait sans casser la maison de verre Go 1.

@md2perpe iota est une façon très limitée d'aborder les énumérations, ce qui fonctionne très bien pour un ensemble limité de circonstances.

  1. Vous avez besoin d'un int
  2. Vous avez seulement besoin d'être cohérent à l'intérieur de votre package, sans représenter l'état externe

Dès que vous avez besoin de représenter une chaîne ou un autre type, ce qui est très courant pour les drapeaux externes, iota ne fonctionne pas pour vous. Si vous voulez faire correspondre une représentation externe/de base de données, je n'utiliserais pas iota , car alors la commande dans le code source est importante et la réorganisation entraînerait des problèmes d'intégrité des données.

Ce n'est pas seulement une question de commodité pour raccourcir le code. Il s'agit d'une proposition qui permettra l'intégrité des données d'une manière qui n'est pas applicable par le langage actuel.

@ianlancetaylor

Par exemple, les gens s'attendraient-ils à pouvoir itérer sur l'ensemble des valeurs d'énumération valides, et si oui, comment feraient-ils cela ?

Je pense que c'est un cas d'utilisation solide, comme mentionné par @bep. Je pense que l'itération ressemblerait à une boucle Go standard, et je pense qu'elles boucleraient dans l'ordre dans lequel elles ont été définies.

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

Si Go devait ajouter quelque chose de plus que iota, à ce stade, pourquoi ne pas opter pour des types de données algébriques ?

Par extension de l'ordre selon l'ordre de définition, et en suivant l'exemple de protobuf, je pense que la valeur par défaut du champ serait le premier champ défini.

@bep Pas aussi pratique, mais vous pouvez obtenir toutes ces propriétés :

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

Je ne pense pas que les énumérations vérifiées au moment de la compilation soient une bonne idée. Je crois que go a à peu près ça en ce moment . Mon raisonnement est

  • Les énumérations vérifiées au moment de la compilation ne sont ni compatibles en amont ni en aval pour le cas d'ajouts ou de suppressions. #18130 consacre des efforts considérables pour aller vers l'activation de la réparation progressive du code ; les énumérations détruiraient cet effort ; tout paquet qui veut changer un ensemble d'énumérations cassera automatiquement et de force tous ses importateurs.
  • Contrairement à ce que prétend le commentaire d'origine, protobuf (pour cette raison spécifique) ne vérifie pas réellement la validité des champs enum. proto2 spécifie qu'une valeur inconnue pour un enum doit être traitée comme un champ inconnu et proto3 spécifie même que le code généré doit avoir un moyen de les représenter avec la valeur encodée (exactement comme go le fait actuellement avec les fake-enums)
  • Au final, ça n'apporte pas grand chose. Vous pouvez obtenir une stringification en utilisant l'outil Stringer. Vous pouvez obtenir une itération en ajoutant une sentinelle MaxValidFoo const (mais voir la mise en garde ci-dessus. Vous ne devriez même pas avoir l'exigence). Vous ne devriez tout simplement pas avoir les deux const-decls en premier lieu. Intégrez simplement un outil dans votre CI qui vérifie cela.
  • Je ne crois pas que d'autres types que les ints soient réellement nécessaires. L'outil stringer devrait déjà couvrir la conversion vers et depuis les chaînes ; au final, le code généré serait équivalent à ce qu'un compilateur générerait de toute façon (à moins que vous ne suggériez sérieusement que toute comparaison sur "string-enums" itérerait les octets…)

Dans l'ensemble, juste un énorme -1 pour moi. Non seulement cela n'ajoute rien; ça fait mal activement.

Je pense que l'implémentation actuelle de l'énumération dans Go est très simple et fournit suffisamment de vérifications du temps de compilation. En fait, je m'attends à une sorte d'énumérations Rust avec une correspondance de modèle de base, mais cela brise peut-être les garanties Go1.

Étant donné que les énumérations sont un cas particulier de types de somme et que la sagesse commune est que nous devrions utiliser des interfaces pour simuler les types de somme, la réponse est clairement https://play.golang.org/p/1BvOakvbj2

(si ce n'est pas clair : oui, c'est une blague - à la manière d'un programmeur classique, je suis à un écart).

Sérieusement, pour les fonctionnalités décrites dans ce fil, des outils supplémentaires seraient utiles.

Comme l'outil stringer, un outil "ranger" pourrait générer l'équivalent de la fonction Iter dans le code que j'ai lié ci-dessus.

Quelque chose pourrait générer des implémentations {Binary,Text}{Marshaler,Unmarshaler} pour les rendre plus faciles à envoyer sur le réseau.

Je suis sûr qu'il y a beaucoup de petites choses comme ça qui seraient très utiles à l'occasion.

Il existe des outils de vérification/linter pour la vérification de l'exhaustivité des types de somme simulés avec des interfaces. Il n'y a aucune raison pour qu'il n'y en ait pas pour les énumérations iota qui vous indiquent quand des cas sont manqués ou que des constantes non typées invalides sont utilisées (peut-être devrait-il simplement signaler autre chose que 0 ?).

Il y a certainement place à l'amélioration sur ce front, même sans changement de langage.

Les énumérations compléteraient le système de types déjà établi. Comme les nombreux exemples de ce numéro l'ont montré, les blocs de construction pour les énumérations sont déjà présents. Tout comme les canaux sont des abstractions de haut niveau construites sur des types plus primitifs, les énumérations doivent être construites de la même manière. Les humains sont arrogants, maladroits et oublieux, des mécanismes comme les énumérations aident les programmeurs humains à faire moins d'erreurs de programmation.

@bep Je dois être en désaccord avec vos trois points. Les énumérations idiomatiques Go ressemblent fortement aux énumérations C, qui n'ont aucune itération de valeurs valides, n'ont aucune conversion automatique en chaînes et n'ont pas nécessairement d'identité distincte.

L'itération est agréable à avoir, mais dans la plupart des cas, si vous voulez une itération, il est bon de définir des constantes pour les première et dernière valeurs. Vous pouvez même le faire d'une manière qui ne nécessite pas de mise à jour lorsque vous ajoutez de nouvelles valeurs, car iota passera automatiquement à la fin. La situation où la prise en charge de la langue ferait une différence significative est lorsque les valeurs de l'énumération ne sont pas contiguës.

La conversion automatique en chaîne n'est qu'une petite valeur : en particulier dans cette proposition, les valeurs de chaîne doivent être écrites pour correspondre aux valeurs int, il y a donc peu à gagner à écrire explicitement un tableau de valeurs de chaîne vous-même. Dans une autre proposition, cela pourrait valoir plus, mais il y a des inconvénients à forcer les noms de variables à correspondre également aux représentations de chaînes.

Enfin, je ne suis même pas sûr que l'identité distincte soit une fonctionnalité utile du tout. Les énumérations ne sont pas des types de somme comme dans, disons, Haskell. Ce sont des numéros nommés. L'utilisation d'énumérations comme valeurs d'indicateur, par exemple, est courante. Par exemple, vous pouvez avoir ReadWriteMode = ReadMode | WriteMode et c'est une chose utile. Il est tout à fait possible d'avoir également d'autres valeurs, par exemple vous pourriez avoir DefaultMode = ReadMode . Ce n'est pas comme si une méthode pouvait empêcher quelqu'un d'écrire const DefaultMode = ReadMode dans tous les cas ; à quoi sert-il d'exiger que cela se produise dans une déclaration distincte ?

@bep Je dois être en désaccord avec vos trois points. Les énumérations idiomatiques Go ressemblent fortement aux énumérations C, qui n'ont aucune itération de valeurs valides, n'ont aucune conversion automatique en chaînes et n'ont pas nécessairement d'identité distincte.

@alercah , s'il vous plaît, n'introduisez pas ce idomatic Go dans une discussion comme un soi-disant "argument gagnant" ; Go n'a pas d'énumérations intégrées, donc parler de certains idoms inexistants n'a pas de sens.

Go a été conçu pour être un meilleur C/C++ ou un Java moins verbeux , donc le comparer à ce dernier aurait plus de sens. Et Java a un Enum type intégré ("Les types d'énumération du langage de programmation Java sont beaucoup plus puissants que leurs homologues dans d'autres langages.") : https://docs.oracle.com/javase/tutorial/java /javaOO/enum.html

Et, même si vous n'êtes peut-être pas d'accord avec la "partie beaucoup plus puissante", le type Java Enum possède les trois fonctionnalités que j'ai mentionnées.

Je peux comprendre l'argument selon lequel Go est plus léger, plus simple, etc., et qu'un compromis doit être pris pour que cela reste ainsi, et j'ai vu des solutions de contournement hacky dans ce fil qui fonctionnent, mais un ensemble de iota ints ne font pas à eux seuls une énumération.

Les énumérations et les conversions automatiques de chaînes sont de bons candidats pour la fonctionnalité « go generate ». Nous avons déjà des solutions. Les énumérations Java sont quelque chose au milieu des énumérations classiques et des types de somme. C'est donc une mauvaise conception du langage à mon avis.
La chose à propos de Go idiomatique est la clé, et je ne vois pas de bonnes raisons de copier toutes les fonctionnalités de la langue X vers la langue Y, simplement parce que quelqu'un le connaît.

Les types d'énumération du langage de programmation Java sont beaucoup plus puissants que leurs homologues dans d'autres langages

C'était vrai il y a dix ans. Voir la mise en œuvre moderne et sans coût d'Option dans Rust optimisée par les types de somme et la correspondance de modèles.

La chose à propos de Go idiomatique est la clé, et je ne vois pas de bonnes raisons de copier toutes les fonctionnalités de la langue X vers la langue Y, simplement parce que quelqu'un le connaît.

Notez que je ne suis pas trop en désaccord avec les conclusions données ici, mais l'utilisation de _ Go idiomatique_ met Go sur un piédestal artistique. La plupart des logiciels de programmation sont assez ennuyeux et pratiques. Et souvent, il vous suffit de remplir une liste déroulante avec une énumération ...

//go:generate enumerator Foo,Bar
Écrit une fois, disponible partout. Notez que l'exemple est abstrait.

@bep Je pense que vous avez mal lu le commentaire d'origine. "Go idiomatic enums" était censé faire référence à la construction actuelle consistant à utiliser le type Foo int + const-decl + iota, je crois, pour ne pas dire "tout ce que vous proposez n'est pas idiomatique".

@rsc En ce qui concerne l'étiquette Go2 , cela va à l'encontre de mon raisonnement pour soumettre cette proposition. # 19412 est une proposition de types de somme complète, qui est un sur-ensemble plus puissant que ma simple proposition d'énumération ici, et je préférerais voir cela dans Go2. De mon point de vue, la probabilité que Go2 se produise dans les 5 prochaines années est infime, et je préférerais voir quelque chose se produire dans un délai plus court.

Si ma proposition d'un nouveau mot clé réservé enum est impossible pour BC, il existe encore d'autres moyens de l'implémenter, qu'il s'agisse d'une intégration complète du langage ou d'outils intégrés à go vet . Comme je l'ai dit à l'origine, je ne suis pas particulier sur la syntaxe, mais je crois fermement que ce serait un ajout précieux à Go aujourd'hui sans ajouter de charge cognitive importante pour les nouveaux utilisateurs.

Un nouveau mot-clé n'est pas possible avant Go 2. Ce serait une violation manifeste de la garantie de compatibilité Go 1.

Personnellement, je ne vois pas encore les arguments convaincants pour enum, ou, d'ailleurs, pour les types sum, même pour Go 2. Je ne dis pas qu'ils ne peuvent pas arriver. Mais l'un des objectifs du langage Go est la simplicité du langage. Il ne suffit pas qu'une fonctionnalité linguistique soit utile ; toutes les fonctionnalités du langage sont utiles - si elles n'étaient pas utiles, personne ne les proposerait. Afin d'ajouter une fonctionnalité à Go, la fonctionnalité doit avoir suffisamment de cas d'utilisation convaincants pour que cela vaille la peine de compliquer le langage. Les cas d'utilisation les plus convaincants sont le code qui ne peut pas être écrit sans la fonctionnalité, du moins maintenant sans grande maladresse.

J'aimerais voir des énumérations dans Go. Je me retrouve constamment à vouloir restreindre mon API exposée (ou à travailler avec une API restreinte en dehors de mon application) dans laquelle il existe un nombre limité d'entrées valides. Pour moi, c'est l'endroit idéal pour une énumération.

Par exemple, je pourrais créer une application cliente qui se connecte à une sorte d'API de style RPC et possède un ensemble spécifié d'actions/opcodes. Je peux utiliser const s pour cela, mais rien n'empêche quiconque (moi y compris !) d'envoyer simplement un code invalide.

De l'autre côté de cela, si j'écris le côté serveur pour cette même API, ce serait bien de pouvoir écrire une instruction switch sur l'énumération, cela déclencherait une erreur de compilation (ou au moins quelques go vet avertissements) si toutes les valeurs possibles de l'énumération ne sont pas vérifiées (ou au moins un default: existe).

Je pense que cela (énumérations) est un domaine dans lequel Swift a vraiment bien compris .

Je pourrais créer une application cliente qui se connecte à une sorte d'API de style RPC et possède un ensemble spécifié d'actions/opcodes. Je peux utiliser consts pour cela, mais rien n'empêche quiconque (moi y compris !) d'envoyer simplement un code invalide.

C'est une idée horrible à résoudre avec des énumérations. Cela signifierait que vous ne pouvez plus jamais ajouter une nouvelle valeur d'énumération, car soudainement les RPC pourraient échouer ou vos données deviendront illisibles lors de la restauration. La raison pour laquelle proto3 exige que le code enum généré prenne en charge une valeur "code inconnu" est qu'il s'agit d'une leçon apprise par la douleur (comparez-la avec la façon dont proto2 a résolu cela, ce qui est mieux, mais toujours très mauvais). Vous voulez que l'application soit capable de gérer ce cas avec élégance.

@Merovius Je respecte votre opinion, mais je suis poliment en désaccord. S'assurer que seules des valeurs valides sont utilisées est l'une des principales utilisations des énumérations.

Les énumérations ne conviennent pas à toutes les situations, mais elles conviennent à certaines ! Une bonne gestion des versions et des erreurs devrait pouvoir gérer de nouvelles valeurs dans la plupart des situations.

Pour traiter avec des processus externes ayant un état uh-oh est un must, certainement.

Avec les énumérations (ou les types de somme plus généraux et utiles), vous pouvez ajouter un code "inconnu" explicite à la somme/énumération que le compilateur vous oblige à gérer (ou simplement gérer cette situation entièrement au point de terminaison si tout ce que vous pouvez faire est enregistrez-le et passez à la requête suivante).

Je trouve les types de somme plus utiles à l'intérieur d'un processus lorsque je sais que j'ai X cas que je sais que je dois traiter. Pour le petit X, ce n'est pas difficile à gérer, mais pour le grand X, j'apprécie que le compilateur me crie dessus, surtout lors du refactoring.

À travers les limites de l'API, les cas d'utilisation sont moins nombreux et il faut toujours pécher par excès d'extensibilité, mais parfois vous avez quelque chose qui ne peut vraiment jamais être qu'une des X choses, comme avec un AST ou des exemples plus triviaux comme un "jour de la valeur "semaine" où la plage est à peu près réglée à ce stade (jusqu'au choix du système calendaire).

@jimmyfrasche Je pourrais vous donner le jour de la semaine, mais pas l'AST. Les grammaires évoluent. Ce qui pourrait être invalide aujourd'hui pourrait être totalement valide demain et cela pourrait impliquer l'ajout de nouveaux types de nœuds à l'AST. Avec les types somme vérifiés par le compilateur, cela ne serait pas possible sans ruptures.

Et je ne vois pas pourquoi cela ne peut pas simplement être résolu par un contrôle vétérinaire ; vous offrant un contrôle statique parfaitement adapté de caisses exhaustives et me donnant la possibilité de réparations progressives.

Je joue avec la mise en œuvre d'un client pour une API serveur. Certains des arguments et des valeurs de retour sont des énumérations dans l'API. Il existe 45 types d'énumération au total.

L'utilisation de constantes énumérées dans Go n'est pas réalisable dans mon cas car certaines des valeurs de différents types d'énumération partagent le même nom. Dans l'exemple ci-dessous, Destroy apparaît deux fois afin que le compilateur émette l'erreur Destroy redeclared in this block .

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

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

Par conséquent, je devrai proposer une représentation différente. Idéalement, celui qui permet à un IDE d'afficher les valeurs possibles pour un type donné afin que les utilisateurs du client aient plus de facilité à l'utiliser. Avoir enum comme citoyen de première classe à Go satisferait cela.

@kongslund Je sais que ce n'est pas une implémentation parfaite, mais je viens de créer un générateur de code qui pourrait vous intéresser. Il vous suffit de déclarer votre énumération dans un commentaire au-dessus de la déclaration de type et de générer le reste pour vous.

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

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

Génèrerait

const(
  _ TaskAllowedOperations = iota
  TaskAllowedOperationsCancel
  TaskAllowedOperationsDestroy
)

const(
  _ OnNormalExit = iota
  OnNormalExitDestroy
  OnNormalExitRestart
)

La meilleure partie est qu'il générerait des méthodes String() qui excluent le préfixe, vous permettant d'analyser "Destroy" comme TaskAllowedOperations ou OnNormalExit .

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

Maintenant que la prise est à l'écart...

Personnellement, cela ne me dérange pas que les énumérations ne soient pas incluses dans le langage go, ce qui n'était pas mon sentiment initial à ce sujet. Lors de mon premier voyage, j'ai souvent eu une réaction confuse quant à la raison pour laquelle tant de choix avaient été faits. Mais après avoir utilisé le langage, il est agréable d'avoir la simplicité à laquelle il adhère, et si quelque chose de plus est nécessaire, il y a de fortes chances que quelqu'un d'autre en ait également eu besoin et ait créé un package génial pour résoudre ce problème particulier. Garder la quantité de cruft à ma discrétion.

De nombreux points valables ont été soulevés dans cette discussion, certains en faveur du support enum et aussi beaucoup contre (du moins dans la mesure où la proposition disait quoi que ce soit sur ce que sont les "énumérations" en premier lieu). Quelques trucs qui m'ont marqué :

  • L'exemple d'introduction (Enums in Go aujourd'hui) est trompeur : ce code est généré et presque personne n'écrirait de code Go comme celui-ci à la main. En fait, la suggestion (à quoi cela pourrait ressembler avec le support linguistique) est beaucoup plus proche de ce que nous faisons déjà en Go.

  • @jediorange mentionne que Swift "a vraiment (énumérés) raison": Quoi qu'il en soit, mais les énumérations Swift sont une bête étonnamment compliquée, mélangeant toutes sortes de concepts. En Go, nous évitons délibérément les mécanismes qui se chevauchent avec d'autres caractéristiques du langage et obtiennent en retour plus d'orthogonalité. La conséquence pour un programmeur est qu'il n'a pas à décider quelle fonctionnalité utiliser : une énumération ou une classe, ou un type somme (si nous en avions), ou une interface.

  • Le point de @ianlancetaylor sur l'utilité des fonctionnalités linguistiques ne doit pas être pris à la légère. Il y a un million de fonctionnalités utiles ; la question est de savoir lesquels sont vraiment convaincants et valent leur coût (de complexité supplémentaire du langage et donc de lisibilité, et de mise en œuvre).

  • Comme point mineur, les constantes définies par iota dans Go ne sont bien sûr pas limitées aux entiers. Tant qu'il s'agit de constantes, elles sont limitées aux types de base (éventuellement nommés) (y compris les flottants, les booléens, les chaînes : https://play.golang.org/p/lhd3jqqg5z).

  • @merovius fait de bons points sur les limites des vérifications (statiques !) au moment de la compilation. Je doute fort que les énumérations qui ne peuvent pas être étendues conviennent dans des situations où l'extension est souhaitable ou attendue (toute surface d'API à longue durée de vie évolue avec le temps).

Ce qui m'amène à quelques questions sur cette proposition auxquelles je pense qu'il faut répondre avant qu'il puisse y avoir des progrès significatifs :

1) Quelles sont les attentes réelles pour les énumérations telles que proposées ? @bep mentionne l'énumérabilité, l'itérabilité, les représentations de chaînes, l'identité. Y a t-il plus? Y en a-t-il moins ?

2) En supposant que la liste en 1), les énumérations peuvent-elles être étendues ? Si c'est le cas, comment? (dans le même colis ? un autre colis ?) S'ils ne peuvent pas être prolongés, pourquoi pas ? Pourquoi n'est-ce pas un problème dans la pratique ?

3) Espace de noms : dans Swift, un type enum introduit un nouvel espace de noms. Il existe une machinerie importante (sucre syntaxique, déduction de type) telle que le nom de l'espace de noms n'a pas à être répété partout. Par exemple, pour les valeurs enum d'un mois enum, dans le bon contexte, on peut écrire .January plutôt que Month.January (ou pire, MyPackage.Month.January). Un espace de noms enum est-il nécessaire ? Si oui, comment un espace de noms enum est-il étendu ? Quel type de sucre syntaxique est nécessaire pour que cela fonctionne en pratique ?

4) Les valeurs enum sont-elles constantes ? Des valeurs immuables ?

5) Quels types d'opérations sont possibles sur les valeurs d'énumération (par exemple, en plus de l'itération) : puis-je en déplacer une vers l'avant, une vers l'arrière ? Nécessite-t-il des fonctions ou des opérateurs intégrés supplémentaires ? (Toutes les itérations peuvent ne pas être dans l'ordre). Que se passe-t-il si l'on avance au-delà de la dernière valeur d'énumération ? Est-ce une erreur d'exécution ?

(J'ai corrigé ma formulation du paragraphe suivant dans https://github.com/golang/go/issues/19814#issuecomment-322771922. Toutes mes excuses pour le choix négligent des mots ci-dessous.)

Sans essayer de répondre réellement à ces questions, cette proposition n'a pas de sens ("Je veux des énumérations qui font ce que je veux" n'est pas une proposition).

Sans essayer de répondre réellement à ces questions, cette proposition n'a aucun sens

@griesemer Vous avez un excellent ensemble de points/questions - mais qualifier cette proposition de non-sens pour ne pas répondre à ces questions n'a pas de sens. La barre de contribution est placée haut dans ce projet, mais il devrait être permis de _proposer quelque chose_ sans avoir un doctorat en compilateurs, et une proposition ne devrait pas avoir besoin d'être une conception _prête à mettre en œuvre_.

  • Go avait besoin de cette proposition car elle a lancé une discussion indispensable => valeur et signification
  • Si cela mène aussi à la proposition #21473 => valeur et signification

L'exemple d'introduction (Enums in Go aujourd'hui) est trompeur : ce code est généré et presque personne n'écrirait de code Go comme celui-ci à la main. En fait, la suggestion (à quoi cela pourrait ressembler avec le support linguistique) est beaucoup plus proche de ce que nous faisons déjà en Go.

@griesemer Je dois être en désaccord. Je n'aurais pas dû laisser la majuscule complète dans le nom de la variable Go, mais il existe de nombreux endroits où le code manuscrit semble presque identique à ma suggestion, écrite par des Googleurs que je respecte dans la communauté Go. Nous suivons assez souvent le même modèle dans notre base de code. Voici un exemple tiré de la bibliothèque Google Cloud Go.

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

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

Ils utilisent la même construction à plusieurs endroits.
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

Il y a eu une discussion plus tard sur la façon dont vous pouvez rendre les choses plus concises si vous êtes d'accord en utilisant iota , ce qui peut être utile en soi, mais pour un cas d'utilisation limité. Voir mon commentaire précédent pour plus de détails. https://github.com/golang/go/issues/19814#issuecomment -290948187

@bep Point juste ; Je m'excuse pour mon choix de mots imprudent. Permettez-moi de réessayer, en espérant que mon dernier paragraphe ci-dessus soit rédigé de manière plus respectueuse et plus claire cette fois :

Afin de pouvoir faire des progrès significatifs, je pense que les partisans de cette proposition devraient essayer d'être un peu plus précis sur ce qu'ils considèrent comme des caractéristiques importantes des énumérations (par exemple en répondant à certaines des questions dans https://github. com/golang/go/issues/19814#issuecomment-322752526). D'après la discussion jusqu'à présent, les fonctionnalités souhaitées ne sont décrites que de manière assez vague.

Peut-être que dans un premier temps, il serait vraiment utile d'avoir des études de cas qui montrent comment le Go existant est (significativement) court et comment les énumérations résoudraient un problème mieux/plus rapidement/plus clairement, etc. Voir aussi l' excellent discours de @rsc à Gophercon concernant les changements de langue Go2.

@derekperkins J'appellerais ces définitions constantes (tapées), pas des énumérations. Je suppose que notre désaccord est dû à une compréhension différente de ce qu'un "énumération" est censé être, d'où mes questions ci-dessus.

(Mon précédent https://github.com/golang/go/issues/19814#issuecomment-322774830 aurait dû aller à @derekperkins bien sûr, pas à @derekparker. La saisie semi-automatique m'a vaincu.)

À en juger par le commentaire de @derekperkins et répondant partiellement à mes propres questions, je suppose qu'un "énumération" Go devrait avoir au moins les qualités suivantes :

  • possibilité de regrouper un ensemble de valeurs sous un (nouveau) type
  • faciliter la déclaration des noms (et des valeurs correspondantes, le cas échéant) avec ce type, avec un minimum de surcharge syntaxique ou passe-partout
  • possibilité de parcourir ces valeurs dans l'ordre de déclaration croissant

Est-ce que ça sonne bien? Si oui, que faut-il encore ajouter à cette liste ?

Vos questions sont toutes bonnes.

Quelles sont les attentes réelles pour les énumérations telles que proposées ? @bep mentionne l'énumérabilité, l'itérabilité, les représentations de chaînes, l'identité. Y a t-il plus? Y en a-t-il moins ?

En supposant que la liste en 1), les énumérations peuvent-elles être étendues ? Si c'est le cas, comment? (dans le même colis ? un autre colis ?) S'ils ne peuvent pas être prolongés, pourquoi pas ? Pourquoi n'est-ce pas un problème dans la pratique ?

Je ne pense pas que les énumérations puissent être étendues pour deux raisons:

  1. Les énumérations doivent représenter la gamme complète des valeurs acceptables, donc les étendre n'a pas de sens.
  2. Tout comme les types Go normaux ne peuvent pas être étendus dans des packages externes, cela maintient les mêmes mécanismes et attentes des développeurs

Espace de noms : dans Swift, un type enum introduit un nouvel espace de noms. Il existe une machinerie importante (sucre syntaxique, déduction de type) telle que le nom de l'espace de noms n'a pas à être répété partout. Par exemple, pour les valeurs enum d'un mois enum, dans le bon contexte, on peut écrire .January plutôt que Month.January (ou pire, MyPackage.Month.January). Un espace de noms enum est-il nécessaire ? Si oui, comment un espace de noms enum est-il étendu ? Quel type de sucre syntaxique est nécessaire pour que cela fonctionne en pratique ?

Je comprends comment l'espacement des noms est apparu, comme tous les exemples que j'ai mentionnés préfixent avec le nom du type. Même si je ne m'opposerais pas à ce que quelqu'un se sente fortement à l'idée d'ajouter un espacement de noms, je pense que c'est hors de portée pour cette proposition. Le préfixe s'intègre très bien dans le système actuel.

Les valeurs enum sont-elles constantes ? Des valeurs immuables ?

Je penserais aux constantes.

Quels types d'opérations sont possibles sur les valeurs d'énumération (par exemple, en plus de l'itération) : puis-je en déplacer une vers l'avant, une vers l'arrière ? Nécessite-t-il des fonctions ou des opérateurs intégrés supplémentaires ? (Toutes les itérations peuvent ne pas être dans l'ordre). Que se passe-t-il si l'on avance au-delà de la dernière valeur d'énumération ? Est-ce une erreur d'exécution ?

Je choisirais par défaut les pratiques Go standard pour les tranches/tableaux (pas les cartes). Les valeurs d'énumération seraient itérables en fonction de l'ordre de déclaration. Au minimum, il y aurait un support de gamme. Je m'éloigne de l'accès aux énumérations via l'index, mais je n'y tiens pas vraiment. Ne pas prendre en charge cela devrait éliminer l'erreur d'exécution potentielle.

Il y aurait une nouvelle erreur d'exécution (panique ?) causée par l'attribution d'une valeur non valide à une énumération, que ce soit par attribution directe ou par transtypage.

Si je résume cela correctement, les valeurs enum telles que vous les proposez sont comme des constantes typées (et comme les constantes, elles peuvent avoir des valeurs constantes définies par l'utilisateur) mais:

  • ils définissent également le type enum associé aux valeurs enum (sinon ce ne sont que des constantes) dans la même déclaration
  • il est impossible de convertir/créer une valeur enum d'un type enum existant en dehors de sa déclaration
  • il est possible de les parcourir

Est-ce que ça te paraît correct? (Cela correspondrait à l'approche classique des langages envers les énumérations, lancée il y a environ 45 ans par Pascal).

Oui, c'est exactement ce que je propose.

Qu'en est-il des instructions switch ? AIUI qui est l'un des principaux moteurs de la proposition.

Pouvoir activer une énumération est implicite, je pense, puisque vous pouvez activer pratiquement n'importe quoi. J'aime que Swift ait des erreurs si vous n'avez pas entièrement satisfait l'énumération de votre commutateur, mais cela pourrait être géré par le vétérinaire

@jediorange Je faisais spécifiquement référence à la question de cette dernière partie, de savoir s'il devrait y avoir ou non un contrôle d'exhaustivité (dans l'intérêt de garder la proposition complète). « Non » est, bien sûr, une réponse parfaitement correcte.

Le message original de ce numéro mentionne les protobufs comme facteur de motivation. J'aimerais signaler explicitement qu'avec la sémantique telle qu'elle est donnée maintenant, le compilateur protobuf devrait créer un cas supplémentaire "non reconnu" pour toute énumération (impliquant un schéma de manipulation de noms pour éviter les collisions). Il faudrait également ajouter un champ supplémentaire à toute structure générée à l'aide d'énumérations (encore une fois, en modifiant les noms d'une manière ou d'une autre), au cas où la valeur enum décodée ne se trouverait pas dans la plage compilée. Tout comme cela se fait actuellement pour java . Ou, probablement plus probablement, continuez à utiliser int s.

@Merovius Ma proposition originale mentionnait protobufs comme exemple, et non comme motivation principale de la proposition. Vous soulevez un bon point au sujet de cette intégration. Je pense que cela devrait probablement être traité comme une préoccupation orthogonale. La plupart des codes que j'ai vus sont convertis à partir des types protobuf générés en structures au niveau de l'application, préférant les utiliser en interne. Il serait logique pour moi que protobuf puisse rester inchangé, et si les créateurs de l'application veulent les convertir en une énumération Go, ils pourraient gérer les cas extrêmes que vous évoquez dans le processus de conversion.

@derekperkins Quelques questions supplémentaires :

  • Quelle est la valeur zéro d'une variable de type enum qui n'est pas explicitement initialisée ? Je suppose qu'il ne peut pas être nul en général (ce qui complique l'allocation/initialisation de la mémoire).

  • Pouvons-nous faire de l'arithmétique limitée avec des valeurs enum? Par exemple, en Pascal (dans lequel j'ai programmé une fois, il y a bien longtemps), il était étonnamment souvent nécessaire d'itérer par étapes> 1. Et parfois, on voulait calculer la valeur enum.

  • En ce qui concerne l'itération, pourquoi la prise en charge de l'itération produite (et de la chaîne) go generate n'est-elle pas suffisante ?

Quelle est la valeur zéro d'une variable de type enum qui n'est pas explicitement initialisée ? Je suppose qu'il ne peut pas être nul en général (ce qui complique l'allocation/initialisation de la mémoire).

Comme je l'ai mentionné dans la proposition initiale, c'est l'une des décisions les plus délicates à prendre. Si l'ordre de définition est important pour l'itération, je pense qu'il serait également logique que la première valeur définie soit la valeur par défaut.

Pouvons-nous faire de l'arithmétique limitée avec des valeurs enum? Par exemple, en Pascal (dans lequel j'ai programmé une fois, il y a bien longtemps), il était étonnamment souvent nécessaire d'itérer par étapes> 1. Et parfois, on voulait calculer la valeur enum.

Que vous utilisiez des énumérations numériques ou basées sur des chaînes, cela signifie-t-il que toutes les énumérations ont un index basé sur zéro implicite ? La raison pour laquelle j'ai mentionné précédemment que je penche uniquement pour les itérations range prises en charge et non basées sur l'index, c'est que cela n'expose pas l'implémentation sous-jacente, qui pourrait utiliser un tableau ou une carte ou quoi que ce soit en dessous. Je ne prévois pas avoir besoin d'accéder aux énumérations via l'index, mais si vous avez des raisons pour lesquelles cela serait bénéfique, je ne pense pas qu'il y ait une raison de l'interdire.

En ce qui concerne l'itération, pourquoi la prise en charge de l'itération produite (et de la chaîne) go generate n'est-elle pas suffisante ?

L'itération n'est pas mon cas d'utilisation principal personnellement, même si je pense que cela ajoute de la valeur à la proposition. Si c'était le facteur déterminant, peut-être go generate serait suffisant. Cela n'aide pas à garantir la sécurité de la valeur. L'argument Stringer() suppose que la valeur brute va être iota ou int ou un autre type représentant la valeur "réelle". Vous devrez également générer (Un)MarshalJSON , (Un)MarshalBinary , Scanner/Valuer et toute autre méthode de sérialisation que vous pourriez utiliser pour vous assurer que la Stringer a été utilisée pour communiquer contre tout ce que Go utilise en interne.

@griesemer Je pense que je n'ai peut-être pas entièrement répondu à votre question sur l'extensibilité des énumérations, du moins en ce qui concerne l'ajout/la suppression de valeurs. Avoir la possibilité de les modifier est une partie essentielle de cette proposition.

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

tout paquet qui veut changer un ensemble d'énumérations cassera automatiquement et de force tous ses importateurs

Je ne vois pas en quoi cela est différent de tout autre changement d'API de rupture. Il appartient au créateur du package de gérer respectueusement BC, de la même manière que si les types, les fonctions ou les signatures de fonctions changeaient.

Du point de vue de la mise en œuvre, il serait assez complexe de prendre en charge des types dont la valeur par défaut n'était pas tout-bits-zéro. Il n'y a pas de tels types aujourd'hui. Exiger une telle fonctionnalité devrait compter comme une marque contre cette idée.

La seule raison pour laquelle le langage nécessite make pour créer un canal est de préserver cette fonctionnalité pour les types de canaux. Sinon, make pourrait être facultatif, uniquement utilisé pour définir la taille de la mémoire tampon du canal ou pour affecter un nouveau canal à une variable existante.

@derekperkins La plupart des autres modifications de l'API peuvent être orchestrées pour une réparation progressive. Je recommande vraiment de lire la description de Russ Cox, cela rend beaucoup de choses très claires.

Les énumérations ouvertes (comme la construction const+iota actuelle) permettent une réparation progressive, en (par exemple) a) définissant la nouvelle valeur sans l'utiliser, b) mettant à jour les dépendances inverses pour gérer la nouvelle valeur, c) commencer à utiliser la valeur. Ou, si vous souhaitez supprimer une valeur, a) arrêtez d'utiliser la valeur, b) mettez à jour les dépendances inverses pour ne pas mentionner la valeur à supprimer, c) supprimez la valeur.

Avec des énumérations fermées (vérifiées par le compilateur pour l'exhaustivité), cela n'est pas possible. Si vous supprimez la gestion d'une valeur ou en définissez une nouvelle, le compilateur se plaindra immédiatement d'un switch-case manquant. Et vous ne pouvez pas ajouter la gestion d'une valeur avant d'en définir une.

La question n'est pas de savoir si les changements individuels peuvent être considérés comme cassants (ils le peuvent, isolément), mais de savoir s'il existe une séquence incassable de validations sur la base de code distribuée qui ne cassera pas.

Du point de vue de la mise en œuvre, il serait assez complexe de prendre en charge des types dont la valeur par défaut n'était pas tout-bits-zéro. Il n'y a pas de tels types aujourd'hui. Exiger une telle fonctionnalité devrait compter comme une marque contre cette idée.

@ianlancetaylor Je ne pourrai certainement pas parler de l'implémentation complète, mais si les énumérations étaient implémentées comme un tableau basé sur 0 (ce à quoi ressemble @griesemer est en faveur), alors 0 comme l'index semble être il pourrait doubler comme "tout-bits-zéro".

Avec des énumérations fermées (vérifiées par le compilateur pour l'exhaustivité), cela n'est pas possible.

@Merovius Si l'exhaustivité était vérifiée par go vet ou un outil similaire comme suggéré par @jediorange vs appliqué par le compilateur, cela atténuerait-il vos inquiétudes?

@derekperkins À propos de leur nocivité, oui. Pas sur leur manque d'utilité. Les mêmes problèmes de version-skew se produisent également pour la plupart des cas d'utilisation pour lesquels ils sont généralement pris en compte (appels système, protocoles réseau, formats de fichiers, objets partagés…). Il y a une raison pour laquelle proto3 nécessite des énumérations ouvertes et pas proto2 - c'est une leçon tirée de nombreuses pannes et incidents de corruption de données. Même si Google fait déjà très attention à éviter le biais de version. De mon point de vue, les énumérations ouvertes avec des cas par défaut ne sont que la bonne solution. Et à part la prétendue sécurité contre les valeurs invalides, ils n'apportent pas grand-chose à la table, d'après ce que je peux dire.

Tout cela étant dit, je ne suis pas un décideur.

@derekperkins Dans https://github.com/golang/go/issues/19814#issuecomment -322818206, vous confirmez que (de votre point de vue):

  • une déclaration enum déclare un type enum avec des valeurs enum nommées (constantes)
  • il est possible de les parcourir
  • aucune valeur ne peut être ajoutée à l'énumération en dehors de sa déclaration
    Et plus tard : un changement d'énumération doit être (ou peut-être pas) exhaustif (semble moins important)

Dans https://github.com/golang/go/issues/19814#issuecomment -322895247, vous dites que :

  • la première valeur définie devrait probablement être la valeur par défaut (zéro) (notez que cela n'a pas d'importance pour l'itération, c'est important pour l'initialisation de la variable enum)
  • l'itération n'est pas votre motivation première

Et dans https://github.com/golang/go/issues/19814#issuecomment -322903714, vous dites que "la possibilité de les modifier est une partie importante de cette proposition".

Je suis confus : l'itération n'est donc pas une motivation principale, d'accord. Cela laisse au minimum une déclaration enum de valeurs enum qui sont des constantes, et elles ne peuvent pas être étendues en dehors de la déclaration. Mais maintenant, vous dites que la possibilité de les modifier est importante. Qu'est-ce que ça veut dire? Sûrement pas qu'ils puissent être prolongés (ce serait une contradiction). Sont-ils variables ? (Mais alors ce ne sont pas des constantes).

Dans https://github.com/golang/go/issues/19814#issuecomment -322903714, vous dites que les énumérations pourraient être implémentées en tant que tableau basé sur 0. Cela suggère qu'une déclaration enum introduit un nouveau type avec une liste ordonnée de noms d'énumération qui sont des indices constants basés sur 0 dans un tableau de valeurs d'énumération (pour lesquelles l'espace est réservé automatiquement). C'est ce que tu veux dire ? Si tel est le cas, pourquoi ne suffirait-il pas de déclarer simplement un tableau de taille fixe et une liste d'indices de constantes pour l'accompagner? La vérification des limites du tableau garantirait automatiquement que vous ne pouvez pas "étendre" la plage d'énumération, et l'itération serait déjà possible.

Qu'est-ce que je rate?

Je suis confus : l'itération n'est donc pas une motivation principale, d'accord.

J'ai mes propres raisons pour lesquelles je veux des énumérations, tout en essayant de prendre en compte ce que d'autres dans ce fil, y compris @bep et d'autres, ont exprimé comme éléments nécessaires de la proposition.

Cela laisse au minimum une déclaration enum de valeurs enum qui sont des constantes, et elles ne peuvent pas être étendues en dehors de la déclaration. Mais maintenant, vous dites que la possibilité de les modifier est importante. Qu'est-ce que ça veut dire? Sûrement pas qu'ils puissent être prolongés (ce serait une contradiction). Sont-ils variables ? (Mais alors ce ne sont pas des constantes).

Quand je dis de les éditer, c'est au point de vue de @Merovius qu'il s'agit d'énumérations ouvertes. Constantes au moment de la construction, mais pas verrouillées pour toujours.

Dans # 19814 (commentaire), vous dites que les énumérations pourraient être implémentées sous forme de tableau basé sur 0.

C'est juste moi qui spécule au-delà de mon salaire sur la façon dont j'imagine qu'il pourrait être mis en œuvre dans les coulisses, sur la base de votre https://github.com/golang/go/issues/19814#issuecomment -322884746 et https de @ianlancetaylor : //github.com/golang/go/issues/19814#issuecomment -322899668

"Pouvons-nous faire de l'arithmétique limitée avec des valeurs enum ? Par exemple, en Pascal (dans lequel j'ai programmé une fois, il y a bien longtemps), il était étonnamment souvent nécessaire d'itérer par étapes> 1. Et parfois, on voulait calculer la valeur enum."

Je ne sais pas comment vous compteriez faire cela pour toute énumération non entière, d'où ma question de savoir si cette arithmétique exigerait que chaque membre de l'énumération se voie implicitement attribuer un index basé sur l'ordre de déclaration.

Du point de vue de la mise en œuvre, il serait assez complexe de prendre en charge des types dont la valeur par défaut n'était pas tout-bits-zéro. Il n'y a pas de tels types aujourd'hui. Exiger une telle fonctionnalité devrait compter comme une marque contre cette idée.

Encore une fois, je ne sais pas comment fonctionne le compilateur, alors j'essayais juste de continuer la conversation. En fin de compte, je n'essaie pas de proposer quoi que ce soit de radical. Comme vous l'avez mentionné précédemment, "Cela correspondrait à l'approche classique que les langages ont adoptée envers les énumérations, lancée il y a environ 45 ans par Pascal", et cela correspond à la facture.

Pour toute autre personne ayant manifesté son intérêt, n'hésitez pas à participer.

Une autre question est de savoir si l'on peut utiliser ces énumérations pour indexer dans des tableaux ou des tranches. Une tranche est souvent un moyen très efficace et compact de représenter un mappage enum-> value et exiger une carte serait malheureux, je pense.

@derekperkins Ok, j'ai peur que cela nous ramène (ou du moins moi) à la case départ : quel est le problème que vous essayez de résoudre ? Voulez-vous simplement une manière plus agréable d'écrire ce que nous faisons actuellement avec des constantes et peut-être iota (et pour lequel nous utilisons go generate pour obtenir des représentations sous forme de chaîne) ? C'est-à-dire du sucre syntaxique pour une notation que vous trouvez (peut-être) trop lourde ? (C'est une bonne réponse, j'essaie juste de comprendre.)

Vous avez mentionné que vous aviez vos propres raisons de les vouloir, peut-être pourriez-vous expliquer un peu plus quelles sont ces raisons. L'exemple que vous avez donné au tout début n'a pas beaucoup de sens pour moi, mais il me manque probablement quelque chose.

Dans l'état actuel des choses, tout le monde a une compréhension un peu différente de ce que cette proposition ("énumérations") implique, comme cela ressort clairement des différentes réponses : il existe un large éventail de possibilités entre les énumérations Pascal et les énumérations Swift. À moins que vous (ou quelqu'un d'autre) ne décriviez très clairement ce qui est proposé (je ne demande pas une mise en œuvre, attention), il sera difficile de faire des progrès significatifs ou même de simplement débattre des mérites de cette proposition.

Cela a-t-il du sens?

@griesemer Cela a tout à fait du sens et je comprends que la barre soit dépassée dont @rsc a parlé à Gophercon. De toute évidence, vous avez une compréhension beaucoup plus profonde que moi. Dans # 21473, vous avez mentionné que iota pour vars n'était pas implémenté car il n'y avait pas de cas d'utilisation convaincant à l'époque. Est-ce la même raison pour laquelle enum n'a pas été inclus dès le début ? Je serais très intéressé de connaître votre opinion sur la valeur ajoutée ou non de Go, et si c'était le cas, par où commenceriez-vous le processus ?

@derekperkins Concernant votre question dans https://github.com/golang/go/issues/19814#issuecomment -323144075 : À l'époque (dans la conception de Go), nous n'envisagions que des énumérations relativement simples (disons Pascal ou de style C). Je ne me souviens pas de tous les détails, mais il y avait certainement le sentiment qu'il n'y avait pas assez d'avantages pour la machinerie supplémentaire requise pour les énumérations. Nous avons estimé qu'il s'agissait essentiellement de déclarations constantes glorifiées.

Il y a aussi des problèmes avec ces énumérations traditionnelles : il est possible de faire de l'arithmétique avec elles (ce ne sont que des entiers), mais qu'est-ce que cela signifie s'ils sortent de la plage (enum) ? En Go, ce ne sont que des constantes et "hors plage" n'existe pas. Un autre est l'itération : en Pascal, il y avait des fonctions intégrées spéciales (je pense que SUCC et PRED) pour faire avancer et reculer la valeur d'une variable de type enum (en C, on fait juste ++ ou --). Mais le même problème apparaît également ici : que se passe-t-il si l'on dépasse la fin (problème très courant dans une boucle for allant sur des valeurs enum utilisant ++ ou l'équivalent Pascal SUCC). Enfin, une déclaration enum introduit un nouveau type dont les éléments sont les valeurs enum. Ces valeurs ont des noms (ceux définis dans la déclaration enum), mais ces noms sont (en Pascal, C) dans la même portée que le type. C'est un peu insatisfaisant : lors de la déclaration de deux énumérations différentes, on espère pouvoir utiliser le même nom de valeur d'énumération pour chaque type d'énumération sans conflit, ce qui n'est pas possible. Bien sûr, Go ne résout pas cela non plus, mais une déclaration constante ne semble pas non plus introduire un nouvel espace de noms. Une solution plus agréable consiste à introduire un espace de noms avec chaque énumération, mais chaque fois qu'une valeur enum est utilisée, elle doit être qualifiée avec le nom du type enum, ce qui est ennuyeux. Swift résout ce problème en déduisant le type enum si possible, puis on peut utiliser le nom de la valeur enum préfixé par un point. Mais c'est un peu de la machinerie. Et enfin, parfois (souvent, dans les API publiques), il faut étendre une déclaration enum. Si ce n'est pas possible (vous ne possédez pas le code), il y a un problème. Avec les constantes, ces problèmes n'existent pas.

Il y a probablement plus à cela; c'est juste ce qui me vient à l'esprit. En fin de compte, nous avons décidé qu'il valait mieux émuler les énumérations dans Go en utilisant les outils orthogonaux dont nous disposions déjà : des types d'entiers personnalisés qui rendent les affectations erronées moins probables, et le mécanisme iota (et la capacité de supprimer les expressions d'initialisation répétées) pour le sucre syntaxique.

D'où mes questions : Que cherchez-vous à gagner des déclarations enum spécialisées que nous ne pouvons pas émuler de manière adéquate dans Go avec peu de surcharge syntaxique ? Je peux penser à une énumération et à un type enum qui ne peut pas être étendu en dehors de la déclaration. Je peux penser à plus de capacités pour les valeurs enum, comme dans Swift.

L'énumération pourrait être résolue facilement avec un générateur de go dans Go. Nous avons déjà un cordonnier. Restreindre l'extension est problématique au-delà des limites de l'API. Plus de capacités pour les valeurs enum (disons comme dans Swift) semble très contrairement à Go car elle mélange beaucoup de concepts orthogonaux. En Go, nous y parviendrions probablement en utilisant des blocs de construction élémentaires.

@griesemer Merci pour votre réponse réfléchie. Je ne conteste pas qu'il s'agisse essentiellement de déclarations constantes glorifiées. Avoir la sécurité de type dans Go est génial, et la principale valeur que enum me fournirait personnellement est la sécurité de la valeur. La façon d'imiter cela dans Go aujourd'hui est d'exécuter des fonctions de validation à chaque point d'entrée pour cette variable. C'est verbeux et il est facile de faire des erreurs, mais c'est possible avec le langage tel qu'il est aujourd'hui. J'ai déjà un espace de noms en préfixant le nom du type devant l'énumération, qui bien que verbeux, n'est pas un gros problème.

Personnellement, je n'aime pas la plupart des utilisations de iota . Bien que cool, la plupart du temps, mes valeurs de type enum correspondent à des ressources externes comme une base de données ou une API externe, et je préfère être plus explicite sur le fait que la valeur ne doit pas être modifiée si vous réorganisez. iota n'aide pas non plus pour la plupart des endroits où j'utiliserais un enum parce que j'utiliserais une liste de valeurs de chaîne.

En fin de compte, je ne sais pas combien je peux clarifier davantage cette proposition. J'adorerais s'ils étaient soutenus de la manière qui aurait du sens pour Go. Indépendamment de l'implémentation exacte, je serais toujours capable de les utiliser et ils rendraient mon code plus sûr.

Je pense que la façon canonique Go fait des énumérations aujourd'hui (comme on le voit dans https://github.com/golang/go/issues/19814#issuecomment-290909885) est assez proche de la correction.
Il y a quelques inconvénients :

  1. Ils ne peuvent pas être répétés
  2. Ils n'ont pas de représentation sous forme de chaîne
  3. Les clients peuvent introduire de fausses valeurs d'énumération

Je vais bien sans #1.
go:generate + stringer peut être utilisé pour #2. Si cela ne gère pas votre cas d'utilisation, faites du type de base de votre "enum" une chaîne au lieu d'un int, et utilisez des valeurs constantes de chaîne.

3 est le plus difficile à gérer avec le Go d'aujourd'hui. J'ai une proposition stupide qui pourrait bien gérer cela.

Ajoutez un mot-clé explicit à une définition de type. Ce mot-clé interdit les conversions dans ce type sauf pour les conversions dans les blocs const du package dans lequel ce type est défini. (Ou restricted ? Ou peut-être enum signifie explicit type ?)

En réutilisant l'exemple que j'ai cité ci-dessus,

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

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

Il y a des conversions de int à SearchRequest à l'intérieur du bloc const . Mais seul l'auteur du package peut introduire une nouvelle valeur SearchRequest, et il est peu probable qu'il en introduise une accidentellement (en passant un int à une fonction qui attend un SearchRequest , par exemple).

Je ne propose pas vraiment activement cette solution, mais je pense que ne pas construire accidentellement un invalide est la propriété saillante des énumérations qui ne peuvent pas être capturées dans Go aujourd'hui (à moins que vous n'utilisiez la structure avec un route de champ non exportée).

Je pense que le risque intéressant avec les énumérations est avec les constantes non typées. Les personnes qui écrivent une conversion de type explicite savent ce qu'elles font. Je serais prêt à envisager un moyen pour Go d'interdire les conversions de types explicites dans certaines circonstances, mais je pense que c'est tout à fait orthogonal à la notion de types enum. C'est une idée qui s'applique à tout type de type.

Mais les constantes non typées peuvent conduire à la création accidentelle et inattendue d'une valeur du type, d'une manière qui n'est pas vraie pour les conversions de type explicites. Je pense donc que la suggestion de explicit @randall77 peut être simplifiée pour signifier simplement que les constantes non typées ne peuvent pas être implicitement converties en type. Une conversion de type explicite est toujours requise.

Je serais prêt à envisager un moyen pour Go d'interdire les conversions de type explicites dans certaines circonstances

@ianlancetaylor L'interdiction facultative des conversions de type, qu'elles soient explicites ou implicites, résoudrait les problèmes qui m'ont amené à créer cette proposition en premier lieu. Seul le package d'origine serait alors capable de créer et donc de satisfaire n'importe quel type. C'est encore mieux que la solution enum à certains égards, car elle prend en charge non seulement les déclarations const , mais tout type.

@randall77 , @ianlancetaylor Comment faire fonctionner la suggestion explicit avec la valeur zéro de ce type ?

@derekperkins Interdire complètement les conversions de type rendra impossible l'utilisation de ces types dans les encodeurs/décodeurs génériques, comme les packages encoding/* .

@Merovius Je pense que @ianlancetaylor suggère la restriction uniquement pour les conversions implicites (par exemple, affectation d'une constante non typée à un type restreint). La conversion explicite serait toujours possible.

@griesemer Je sais :) Mais j'ai compris que @derekperkins suggérait différemment.

Le fait de ne pas autoriser les conversions explicites ne sape-t-il pas la raison même pour laquelle nous pensons à ce qualificatif "explicite" ? Si quelqu'un peut décider de convertir une valeur arbitraire en un type "explicite", alors nous n'avons pas plus de garantie qu'une valeur donnée est l'une des constantes énumérées que nous n'en avons maintenant.

Je suppose que cela aide pour l'utilisation occasionnelle ou involontaire de constantes non typées, ce qui est peut-être la chose la plus importante.

Je suppose que je me demande si interdire les conversions explicites est dans "l'esprit de Go". Interdire les conversions explicites, c'est faire un grand pas vers la programmation basée sur les types plutôt que sur la programmation basée sur l'écriture de code. Je pense que Go prend clairement position en faveur de ce dernier.

@griesemer @Merovius Je republierai à nouveau la citation de @ianlancetaylor , puisque c'était sa suggestion, pas la mienne.

Je serais prêt à envisager un moyen pour Go d'interdire les conversions de type explicites dans certaines circonstances

@rogpeppe et @Merovius soulèvent de bons points sur les ramifications. Autoriser les conversions explicites mais pas les conversions implicites ne résout pas le problème de la garantie de types valides, mais perdre l'encodage générique serait un inconvénient assez important.

Il y a eu beaucoup de va-et-vient ici, mais je pense qu'il y a eu quelques bonnes idées. Voici un résumé de ce que j'aimerais voir (ou quelque chose de similaire), qui semble correspondre à ce que d'autres ont dit. J'admets ouvertement que je ne suis pas un concepteur de langage ou un programmeur compilateur, donc je ne sais pas si cela fonctionnerait bien.

  1. Énumérations ancrées dans les types de base uniquement (chaîne, uint, int, rune, etc.). Si aucun type de base n'est nécessaire, il pourrait être par défaut uint?
  2. Toutes les valeurs valides de l'énumération doivent être déclarées avec la déclaration de type -- Constants. Les valeurs non valides (non déclarées dans la déclaration de type) ne peuvent pas être converties en type enum.
  3. Représentation automatique des chaînes pour le débogage (sympa d'avoir).
  4. La compilation vérifie l'exhaustivité des instructions switch sur l'énumération. Recommandez éventuellement (via go vet ?) un cas default , même s'il est déjà exhaustif (probablement une erreur) pour les modifications futures.
  5. La valeur zéro doit essentiellement être invalide (pas quelque chose dans la déclaration enum). Personnellement, j'aimerais qu'il soit nil , comme le fait une tranche.

Ce dernier _peut_ être un peu controversé. Et je ne sais pas avec certitude si cela fonctionnerait, mais je pense que cela s'intégrerait sémantiquement - un peu comme on vérifierait une tranche nil , on pourrait mettre des chèques pour un nil valeur d'énumération.

Quant à l'itération, je ne pense pas vraiment que je l'utiliserais un jour, mais je n'y vois pas le mal.

A titre d'exemple de la façon dont il pourrait être déclaré:

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

De plus, le style Swift consistant à déduire le type et à utiliser la "syntaxe à points" serait _sympa_, mais certainement pas nécessaire.

tapez EnumA entier
constante (
Inconnu EnumA = iota
AAA
)


tapez EnumB entier
constante (
Inconnu EnumB = iota
BBB
)

Il ne peut y avoir 2 morceaux de code dans un seul fichier Go, ni dans le même package, ni même un seul est importé d'un autre package.

Veuillez simplement implémenter la manière C# d'implémenter Enum :
type Days enum {sam, dim, lun, mar, mer, jeu, ven}
type Jours enum[int] { Sam:1 , Sun, Tue, Wed, Thu, Fri}
type Days enum[string] { Sat: "Saturay" , Sun:"Sunday" etc}

@KamyarM Comment est-ce mieux que

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

et

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

Je voudrais demander de bien vouloir limiter les commentaires aux nouvelles approches/arguments. De nombreuses personnes sont abonnées à ce fil et l'ajout de bruit/répétition peut être perçu comme un manque de respect de leur temps et de leur attention. Il y a beaucoup de discussions là-haut ↑, y compris des réponses détaillées aux deux commentaires précédents. Vous ne serez pas d'accord avec tout ce qui a été dit et aucune des parties n'aimera les résultats de cette discussion jusqu'à présent - mais le simple fait de l'ignorer n'aidera pas non plus à la faire avancer dans une direction productive.

C'est mieux parce qu'il n'y a pas de problème de conflit de nommage. Prend également en charge la vérification du type de compilateur. L'approche que vous avez mentionnée l'a organisée mieux que rien, mais le compilateur ne vous limite pas sur ce que vous pouvez lui attribuer. Vous pouvez affecter un entier qui n'est pas l'un des jours à un objet de ce type :
var par jours
un =10
le compilateur ne fait rien à ce sujet. Il n'y a donc pas grand intérêt à ce genre d'énumération. à part qu'il est mieux organisé dans des IDE comme GoLand

J'aimerais voir quelque chose comme ça

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

Ou avec une utilisation automatique 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
}

Cela apportera simplicité et facilité d'utilisation :

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

En outre, enum devrait avoir une méthode intégrée pour valider la valeur afin que nous puissions valider quelque chose à partir de l'entrée de l'utilisateur :

if day in WeekDay {
  makeItWorkOn(day)
}

Et des choses simples comme :

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

Pour être honnête, ma syntaxe préférée serait comme ça (KISS):

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

@zoonman Le dernier exemple ne suit pas le principe suivant de Go : une déclaration de fonction commence par func , une déclaration de type commence par type , une déclaration de variable commence par var , ...

@ md2perpe Je n'essaie pas de suivre les principes de "type" Go, j'écris du code tous les jours et le seul principe que je suis - garder les choses simples.
Plus vous devez écrire de code _pour suivre les principes_, plus vous perdez de temps.
TBH Je suis débutant en Go mais il y a beaucoup de choses que je peux critiquer.
Par exemple:

struct User {
  Id uint
  Email string
}

Est plus facile à écrire et à comprendre que

type User struct {
  Id uint
  Email string
}

Je peux vous donner un exemple où le type doit être utilisé :

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

J'écrivais du code en Asm, C, C++, Pascal, Perl, PHP, Ruby, Python, JavaScript, TypeScript, maintenant Go. J'ai vu tout ça. Cette expérience me dit que le code doit être laconique, facile à lire et à comprendre .

Je fais un projet d'apprentissage automatique et j'ai besoin d'analyser un fichier MIDI.
Là, j'ai besoin d'analyser le timecode SMPTE. Je trouve assez difficile d'utiliser la manière idiomatique avec iota, mais cela ne m'arrête pas)

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
)

Bien sûr, j'aurai peut-être besoin d'une vérification de l'exécution avec une programmation défensive...

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

Les énumérations rendent la vie des programmeurs plus simple dans certains cas. Enums n'est qu'un instrument, alors vous l'utilisez correctement, cela peut gagner du temps et augmenter la productivité. Je pense qu'il n'y a aucun problème à implémenter cela dans Go 2 comme dans c++, c# ou d'autres langages. Cet exemple est juste une blague, mais il montre clairement le problème.

@ streeter12 Je ne vois pas comment votre exemple "montre clairement le problème". Comment les énumérations rendraient-elles ce code meilleur ou plus sûr ?

Il existe une classe C # avec l'implémentation de la même logique d'énumération.

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

Avec les énumérations de temps de compilation, je peux :

  1. N'ayez pas de vérifications d'exécution.
  2. Réduction significative des risques d'erreur par équipe (vous ne pouvez pas transmettre une mauvaise valeur au moment de la compilation).
  3. Cela ne contredit pas le concept iota.
  4. Il est plus facile de comprendre la logique que d'avoir un nom pour les constantes (il est important que vos constantes représentent des valeurs de protocole de bas niveau).
  5. Vous pouvez rendre la méthode ToString() analogique pour faire une représentation simple des valeurs. (CONNECTION_ERROR.NO_INTERNET vaut mieux que 0x12). Je connais Stringer, mais il n'y a pas de génération de code explicite avec des énumérations.
  6. Dans certaines langues, vous pouvez obtenir un tableau de valeurs, une plage, etc.
  7. C'est simple à comprendre lors de la lecture du code (pas besoin de calculs en tête).

Après tout, ce ne sont que des outils pour éviter certaines erreurs humaines courantes et économiser les performances.

@ streeter12 Merci d'avoir clarifié ce que vous vouliez dire. Le seul avantage par rapport aux constantes Go ici est que l'on ne peut pas introduire de valeur invalide car le système de type n'acceptera aucune autre valeur que l'une des valeurs enum. C'est certainement agréable à avoir, mais cela a aussi un prix : il n'y a aucun moyen d'étendre cette énumération en dehors de ce code. L'extension d'énumération externe est l'une des principales raisons pour lesquelles, dans Go, nous avons décidé de ne pas utiliser les énumérations standard.

Répondez simplement au besoin de faire en sorte que certaines extensions n'utilisent pas d'énumérations.
FE doit faire en sorte que la machine d'état utilise un modèle d'état au lieu d'énumérations.

Les énumérations ont leur propre portée. Je réalise de grands projets sans aucune énumération. Je pense que c'est une décision d'architecture terrible d'étendre enum en dehors du code de définition. Vous n'avez aucun contrôle sur ce que fait votre collègue et cela fait de drôles d'erreurs)

Et vous avez oublié les énumérations de facteurs humains dans de nombreux cas, ce qui réduit considérablement les erreurs dans les grands projets.

@ streeter12 Malheureusement, la réalité est que les énumérations doivent souvent être étendues.

@griesemer l'extension d'un type enum/sum crée un type séparé et parfois incompatible.

Cela est toujours vrai dans Go même s'il n'y a pas de types explicites pour les énumérations/sommes. Si vous avez un "type enum" dans un package qui attend des valeurs dans {1, 2, 3} et que vous lui transmettez un 4 de votre "type enum étendu", vous avez toujours violé le contrat du "type" implicite.

Si vous avez besoin d'étendre une énumération/somme, vous devez également créer des fonctions de conversion To/From explicites qui gèrent explicitement les cas parfois incompatibles.

Je pense que la déconnexion entre cet argument et les personnes pour cette proposition ou des propositions similaires comme # 19412 est que nous pensons qu'il est étrange que le compromis soit "toujours écrire du code de validation de base que le compilateur pourrait gérer" au lieu de "parfois écrire des fonctions de conversion que vous ' va probablement aussi devoir écrire de toute façon".

Cela ne veut pas dire que l'une ou l'autre des parties a raison ou tort ou que c'est le seul compromis à considérer, mais je voulais identifier un goulot d'étranglement dans la communication entre les parties que j'ai remarqué.

Je pense que la déconnexion entre cet argument et les personnes pour cette proposition ou des propositions similaires comme # 19412 est que nous pensons qu'il est étrange que le compromis soit "toujours écrire du code de validation de base que le compilateur pourrait gérer" au lieu de "parfois écrire des fonctions de conversion que vous ' va probablement aussi devoir écrire de toute façon".

Très bien indiqué

@jimmyfrasche Ce n'est pas ainsi que je décrirais personnellement le compromis. Je dirais que c'est "toujours écrire du code de validation de base que le compilateur pourrait gérer" par rapport à "ajouter un tout nouveau concept au système de type que tout le monde utilisant Go doit apprendre et comprendre".

Ou, permettez-moi de le dire autrement. Autant que je sache, les seules fonctionnalités importantes manquantes dans la version de Go des types énumérés sont qu'il n'y a pas de validation de l'affectation à partir de constantes non typées, il n'y a pas de contrôle sur les conversions explicites, et il n'y a pas de contrôle que toutes les valeurs ont été traitées dans un changer. Il me semble que ces traits sont tous indépendants de la notion de types énumérés. Nous ne devrions pas laisser le fait que d'autres langages ont des types énumérés nous conduire à la conclusion que Go a également besoin de types énumérés. Oui, les types énumérés nous donneraient ces fonctionnalités manquantes. Mais est-il vraiment nécessaire d'ajouter un type entièrement nouveau pour les obtenir ? Et l'augmentation de la complexité du langage en vaut-elle la peine ?

@ianlancetaylor Ajouter de la complexité à la langue est certainement une chose valable à considérer, et "parce qu'une autre langue l'a" n'est certainement pas un argument. Personnellement, je ne pense pas que les types enum en valent la peine. (Leur généralisation, les types de somme, cochent certainement beaucoup de cases pour moi, cependant).

Une manière générale pour un type de se retirer de l'assignabilité serait bien, bien que je ne sois pas sûr de l'utilité de ce serait en dehors des primitives.

Je ne sais pas à quel point le concept de "vérifier toutes les valeurs gérées dans un commutateur" est généralisable, sans un moyen de faire connaître au compilateur la liste complète des valeurs légales. À part les types enum et sum, la seule chose à laquelle je peux penser est quelque chose comme les types de plage d'Ada, mais ceux-ci ne sont pas naturellement compatibles avec les valeurs zéro à moins que 0 ne doive être dans la plage ou que le code soit généré pour gérer les décalages chaque fois qu'ils sont convertis ou reflétés sur. (D'autres langues ont eu des familles de types similaires, certaines dans la famille pascale, mais Ada est la seule qui me vient à l'esprit pour le moment)

Quoi qu'il en soit, je parlais spécifiquement de:

Le seul avantage par rapport aux constantes Go ici est que l'on ne peut pas introduire de valeur invalide car le système de type n'acceptera aucune autre valeur que l'une des valeurs enum. C'est certainement agréable à avoir, mais cela a aussi un prix : il n'y a aucun moyen d'étendre cette énumération en dehors de ce code. L'extension d'énumération externe est l'une des principales raisons pour lesquelles, dans Go, nous avons décidé de ne pas utiliser les énumérations standard.

et

Malheureusement, la réalité est que les énumérations doivent souvent être étendues.

Cet argument ne fonctionne pas pour moi pour les raisons que j'ai indiquées.

@jimmyfrasche Compris ; c'est un problème difficile. C'est pourquoi, dans Go, nous n'avons pas essayé de le résoudre, mais nous avons plutôt fourni un mécanisme pour créer facilement des séquences de constantes sans avoir besoin de répéter la valeur constante.

(Envoyé en retard - visait à répondre à https://github.com/golang/go/issues/19814#issuecomment-349158748)

@griesemer en effet et c'était définitivement le bon choix pour Go 1 mais certaines d'entre elles méritent d'être réévaluées pour Go 2.

Il y a assez dans le langage pour obtenir _presque_ tout ce que l'on voudrait des types enum. Cela nécessite plus de code qu'une définition de type, mais un générateur peut en gérer la majeure partie et vous permet de définir autant ou aussi peu que la situation le permet au lieu de simplement obtenir les pouvoirs fournis avec un type enum.

Cette approche https://play.golang.org/p/7ud_3lrGfx vous donne tout sauf

  1. la sécurité dans le package de définition
  2. la possibilité de pelucher un interrupteur pour être complet

Cette approche peut également être utilisée pour les petits types de sommes simples† mais elle est plus difficile à utiliser, c'est pourquoi je pense que quelque chose comme https://github.com/golang/go/issues/19412#issuecomment -323208336 ajouterait à le langage et il pourrait être utilisé par un générateur de code pour créer des types enum qui évitent les problèmes 1 et 2.

† voir https://play.golang.org/p/YFffpsvx5e pour un croquis de json.Token avec cette construction

nous pensons qu'il est étrange que le compromis soit "toujours écrire du code de validation de base que le compilateur pourrait gérer" au lieu de "écrire parfois des fonctions de conversion que vous devrez probablement aussi écrire de toute façon".

Pour moi - un représentant du camp des farouches partisans de la réparation progressive - cela semble être le bon compromis. Honnêtement, même si nous ne parlons pas de réparation progressive, je considérerais cela comme un meilleur modèle mental.

D'une part, l'énumération de type vérifié ne pourra de toute façon vérifier que les valeurs insérées dans le code source. Si l'énumération se déplace sur un réseau, est persistante sur le disque ou échangée entre les processus, tous les paris sont ouverts (et la plupart des utilisations proposées des énumérations entrent dans cette catégorie). Ainsi, vous ne contournerez de toute façon pas le problème de la gestion des incompatibilités au moment de l'exécution. Et il n'y a pas de comportement général par défaut unique lorsque vous rencontrez une valeur d'énumération non valide. Souvent, vous voudrez peut-être sortir de l'erreur. Parfois, vous voudrez peut-être le contraindre à une valeur par défaut. La plupart du temps, vous souhaitez le conserver et le faire circuler, afin qu'il ne se perde pas lors de la re-sérialisation.

Bien sûr, vous pourriez soutenir qu'il devrait toujours y avoir une limite de confiance, où la validité est vérifiée et le comportement requis est implémenté - et tout ce qui se trouve à l'intérieur de cette limite devrait pouvoir faire confiance à ce comportement. Et le modèle mental semble être que cette frontière de confiance devrait être un processus. Parce que tout le code d'un binaire sera modifié de manière atomique et restera cohérent en interne. Mais ce modèle mental est érodé par l'idée d'une réparation progressive ; soudain, la frontière de confiance naturelle devient un package (ou peut-être un référentiel) en tant qu'unités auxquelles vous appliquez vos réparations atomiques et l'unité à laquelle vous faites confiance pour être cohérente.

Et, personnellement, je trouve que c'est une unité de cohérence très naturelle et formidable. Un paquet doit être juste assez grand pour garder sa sémantique, ses règles et ses conventions dans votre tête. C'est aussi pourquoi les exportations fonctionnent au niveau du package, pas au niveau du type et pourquoi les déclarations de niveau supérieur sont portées au niveau du package, pas au niveau du programme. Cela me semble bien et me permet d'économiser suffisamment pour décider du traitement correct des valeurs d'énumération inconnues au niveau du package également. Avoir une fonction non exportée, qui la vérifie et maintient le comportement souhaité en interne.

Je serais beaucoup plus d'accord avec une proposition selon laquelle chaque commutateur a besoin d'un cas par défaut, qu'avec une proposition d'avoir des énumérations vérifiées par type, y compris des vérifications d'exhaustivité.

@Merovius Le processus du système d'exploitation et le package sont tous deux des limites de confiance, comme vous le dites.

Les informations provenant de l'extérieur du processus doivent être validées à leur entrée et déclassées dans une représentation appropriée pour le processus et des précautions appropriées doivent être prises en cas d'échec. Cela ne s'en va jamais. Je ne vois vraiment rien de spécifique aux types sum/enum là-bas. Vous pourriez dire la même chose des structs - parfois vous obtenez des champs supplémentaires ou trop peu de champs. Les structures sont toujours utiles.

Cela étant dit, avec le type enum, vous pouvez bien sûr inclure des cas spécifiques pour modéliser ces erreurs. Par exemple

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
}

et avec les types de somme, vous pouvez aller plus loin :

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
}

(Le premier n'est pas aussi utile que s'il est contenu dans une structure avec des champs spécifiques aux cas d'erreur, mais la validité de ces champs dépend de la valeur de l'énumération. Le type somme s'en charge car il s'agit essentiellement d'un struct qui ne peut avoir qu'un seul champ défini à la fois.)

Au niveau du package, vous devez toujours gérer la validation de haut niveau, mais la validation de bas niveau est fournie avec le type. Je dirais que réduire le domaine du type aide à garder le paquet petit et dans votre tête. Cela rend également l'intention plus claire pour l'outillage afin que votre éditeur puisse écrire toutes les lignes case X: et vous laisser remplir le code réel ou un linter pourrait être utilisé pour vous assurer que tout le code vérifie tous les cas (vous m'a dissuadé d'avoir l'exhaustivité dans le compilateur plus tôt).

Je ne vois vraiment rien de spécifique aux types sum/enum là-bas. Vous pourriez dire la même chose des structs - parfois vous obtenez des champs supplémentaires ou trop peu de champs. Les structures sont toujours utiles.

Si nous parlons d'énumérations ouvertes (comme celles actuellement construites par iota), alors, bien sûr. Si nous parlons d'énumérations fermées (ce dont les gens parlent généralement lorsqu'ils parlent d'énumérations) ou d'énumérations avec vérification de l'exhaustivité, alors elles sont certainement spéciales. Parce qu'ils ne sont pas extensibles.

L'analogie avec les structs explique cela assez parfaitement : la promesse de compatibilité Go 1 exclut les littéraux de struct sans clé de toute promesse - et par conséquent, l'utilisation de littéraux de struct à clé a été une pratique si fortement considérée comme "la meilleure", que go vet a un contrôle pour cela. La raison est exactement la même : si vous utilisez des littéraux de structure sans clé, les structures ne sont plus extensibles.

Donc oui. Les structures sont exactement comme les énumérations à cet égard. Et nous avons convenu en tant que communauté qu'il est préférable de les utiliser de manière extensible.

Cela étant dit, avec le type enum, vous pouvez bien sûr inclure des cas spécifiques pour modéliser ces erreurs.

Votre exemple ne couvre que la limite de processus (en parlant d'erreurs de réseau), pas la limite de package. Comment les packages se comporteront-ils si j'ajoute un "InvalidInternalState" (pour inventer quelque chose) à FromTheNetwork ? Dois-je réparer leurs commutateurs avant qu'ils ne compilent à nouveau ? Ensuite, ce n'est pas extensible dans le modèle de réparation progressive. Ont-ils besoin d'un cas par défaut pour compiler en premier lieu ? Ensuite, il ne semble pas y avoir de raison d'énumérer.

Encore une fois, avoir des énumérations ouvertes est une question différente. Je serais à bord avec des choses comme

Je dirais que réduire le domaine du type aide à garder le paquet petit et dans votre tête. Cela rend également l'intention plus claire pour l'outillage afin que votre éditeur puisse écrire toutes les lignes de cas X: et vous laisser remplir le code réel ou un linter pourrait être utilisé pour s'assurer que tout le code vérifie tous les cas

Mais pour cela, nous n'avons pas besoin d'énumérations réelles, en tant que types. Un tel outil de linting pourrait également vérifier de manière heuristique les const -déclarations en utilisant iota , où chaque cas est d'un type donné et considérer qu'il s'agit d'une "énumération" et effectuer les vérifications souhaitées. Je serais complètement d'accord avec un outil utilisant ces "énumérations par convention" pour aider l'auto-complétion ou le linting que chaque commutateur doit avoir une valeur par défaut ou même que chaque cas (connu) doit être vérifié. Je ne serais même pas opposé à l'ajout d'un mot-clé enum qui se comporte principalement comme ça; c'est-à-dire qu'un enum est ouvert (peut prendre n'importe quelle valeur entière), vous donne la portée supplémentaire et nécessite d'avoir une valeur par défaut dans n'importe quel commutateur (je ne pense pas qu'ils ajouteraient assez d'iota-enums pour les coûts supplémentaires, mais au moins ils ne nuiraient pas à mon agenda). Si c'est ce qui est proposé, très bien. Mais cela ne semble pas être ce que veulent dire la majorité des partisans de cette proposition (certainement pas le texte initial).

Nous pouvons ne pas être d'accord sur l'importance de maintenir une réparation et une extensibilité progressives - par exemple, beaucoup de gens pensent que le versionnage sémantique est une meilleure solution aux problèmes qu'il résout. Mais si vous les trouvez importants, il est parfaitement valide et raisonnable de considérer les énumérations comme nuisibles ou inutiles. Et c'était la question à laquelle je répondais : comment les gens peuvent raisonnablement faire le compromis d'exiger une vérification partout, au lieu de l'avoir dans le compilateur. Réponse : En valorisant l'extensibilité et l'évolution des API, ce qui rend ces vérifications nécessaires de toute façon sur le site d'utilisation.

De temps en temps, les opposants à enum ont déclaré qu'ils ne sont pas extensibles, nous avons encore besoin de vérifications après la sérialisation/transition, nous pouvons rompre la compatibilité, etc.

Le principal problème que ce ne sont pas des problèmes d'énumération, ce sont vos problèmes de développement et d'architecture.
Vous essayez de donner un exemple où l'utilisation d'énumérations est ridicule, mais examinons certaines situations plus en détail.

Exemple 1. Je suis développeur de bas niveau et j'ai besoin de const pour certaines adresses de registres, de valeurs de protocole de bas niveau établies, etc. À l'heure actuelle, dans Go, je n'ai qu'une seule solution : utiliser consts sans iota, car dans de nombreux cas, ce serait moche . Je peux obtenir plusieurs blocs de constantes pour un paquet et après avoir appuyé sur . J'ai les 20 constantes et si elles ont le même type et des noms similaires, je peux faire des erreurs. Si le projet est grand, vous obtiendrez cette erreur. Pour éviter cela avec la programmation défensive, TDD nous devons offen dupliquer le code de vérification (code en double = erreurs/tests en double dans tous les cas). Avec l'utilisation des transferts, nous n'avons pas de problèmes similaires et les valeurs ne changeront jamais dans ce cas (essayez de trouver une situation dans laquelle les adresses des registres seront modifiées en production :)). Nous vérifions encore parfois la valeur de ce que nous obtenons du fichier/net d'etc. est dans la plage, mais il n'y a aucun problème à rendre cela centrolisé (voir c# Enum.TryParsepar exemple). Avec les énumérations, je gagne du temps de développement et des performances dans ce cas.

Exemple 2. Je développe un petit module avec une logique état/erreurs. Si je rends l'énumération privée, personne ne connaît jamais cette énumération, et vous pouvez la modifier/l'étendre sans problème avec tous les avantages de 1. Si vous avez basé votre code sur une logique privée, quelque chose s'est complètement mal passé lors de votre développement.

Exemple 3. Je développe un module souvent modifié et extensible pour un large éventail d'applications. Ce serait une solution étrange d'utiliser des énumérations ou toute autre constante pour déterminer la logique/l'interface publique. Si vous ajoutez un nouveau numéro d'énumération sur l'architecture client-serveur, vous pouvez planter, mais avec des constantes, vous pouvez obtenir un état imprévisible du modèle et même l'enregistrer sur le disque. Je préfère souvent le crash à l'état imprévisible. Cela nous montre que le problème de copabilité/extension arrière est un problème de nos développements et non des énumérations. Si vous comprenez ce que les énumérations ne sont pas sutables dans ce cas, ne les utilisez pas. Je pense que nous avons assez de compétence pour choisir.

La principale différence entre consts et compile time enum à mon avis est que les enums ont deux contrats principaux.

  1. Contrat de nommage.
  2. Contrat de valeurs.
    Tous les arguments pour et contre ce paragraphe ont été examinés auparavant.
    Si vous utilisez la programmation contractuelle, vous pouvez facilement en comprendre les avantages.

Énumère combien d'autres choses ont leurs inconvénients.
Fe il ne peut pas être changé sans copabylite de frein. Mais si vous connaissez O des principes SOLID, cela s'applique non seulement à l'énumération mais aussi au développement en général. Quelqu'un peut dire, je rends mon programme laid avec une logique parallèle et des structures modifiables. Interdisons les structures mutables ? Au lieu de cela, nous pouvons ajouter des structures mutable/no mutable et laisser les développeurs choisir.

Après tout ce qui a été dit, je tiens à souligner que Iota a aussi ses inconvénients.

  1. Il a toujours le type int,
  2. Vous devez calculer les valeurs dans head. Vous pouvez perdre beaucoup de temps à essayer de calculer des valeurs et à vérifier que tout est correct.
    Avec enums/const, je peux simplement appuyer sur F12 et voir toutes les valeurs.
  3. L'expression Iota est une expression de code que vous devez également tester.
    Dans certains projets, j'ai complètement refusé d'utiliser iota pour ces raisons.

Vous essayez de donner un exemple où l'utilisation d'énumérations est ridicule

Excusez ma franchise, mais après ce commentaire , je ne pense pas que vous ayez beaucoup de terrain sur lequel vous tenir ici.

Et je ne faisais même pas ce que vous dites - c'est-à-dire donner un exemple où l'utilisation d'énumérations est ridicule. J'ai pris un exemple censé montrer à quel point ils sont nécessaires et illustrer à quel point ils font mal.

Nous pouvons raisonnablement ne pas être d'accord, mais nous devrions au moins tous argumenter de bonne foi.

Exemple 1

Je pourrais vous donner des "noms de registre" comme quelque chose qui n'est vraiment pas modifiable, mais en ce qui concerne les valeurs de protocole, je suis catégorique sur le fait que la position de les faire prendre des valeurs arbitraires pour l'extensibilité et la compatibilité est raisonnable. Encore une fois, proto2 -> proto3 contenait exactement ce changement et il l'a fait à partir de l'expérience acquise.

Et de toute façon, je ne vois pas pourquoi un linter ne pourrait pas attraper ça.

J'ai les 20 constantes et si elles ont le même type et des noms similaires, je peux faire des erreurs. Si le projet est grand, vous obtiendrez cette erreur.

Si vous tapez mal des noms, avoir des énumérations fermées ne vous aidera pas. Uniquement si vous n'utilisez pas les noms symboliques et utilisez plutôt int/string-literals.

Exemple 2

Personnellement, j'ai tendance à mettre fermement "paquet unique" sur la ligne de "pas un grand projet". Ainsi, je considère qu'il est beaucoup moins probable que vous oubliez un cas ou que vous changiez un emplacement de code, lors de l'extension d'une énumération.

Et de toute façon, je ne vois pas pourquoi un linter ne pourrait pas attraper ça.

Exemple 3

C'est cependant le cas d'utilisation le plus courant présenté pour les énumérations. Exemple : ce numéro spécifique les utilise comme justification. Un autre cas souvent mentionné est celui des appels système - une architecture client-serveur déguisée. La généralisation de cet exemple est "tout code où deux ou plusieurs composants développés indépendamment échangent de telles valeurs", ce qui est incroyablement large, couvre la grande majorité des cas d'utilisation pour eux et, sous le modèle de réparation progressive, également toute API exportée .

FTR, je n'essaie toujours pas de convaincre qui que ce soit que les énumérations sont nuisibles (je suis sûr que je ne le ferai pas). Juste pour expliquer comment j'en suis arrivé à la conclusion qu'ils le sont et pourquoi je trouve les arguments en leur faveur peu convaincants.

Il a toujours le type int,

iota peut (pas nécessairement, mais peu importe), mais pas les blocs const , ils peuvent avoir une variété de types constants - en fait, un sur-ensemble des implémentations d'énumération les plus couramment proposées.

Vous devez calculer les valeurs dans head.

Encore une fois, vous ne pouvez pas utiliser cela comme argument en faveur des énumérations ; vous pouvez écrire les constantes comme vous le feriez dans une déclaration enum.

L'expression Iota est une expression de code que vous devez également tester.

Toutes les expressions ne doivent pas être testées. Si c'est immédiatement évident, les tests sont exagérés. Si ce n'est pas le cas, notez les constantes, vous le feriez de toute façon dans un test.

iota n'est pas la méthode actuellement recommandée pour faire des énumérations dans Go - les déclarations const sont. iota sert uniquement de moyen plus général d'économiser la frappe lors de l'écriture de déclarations const consécutives ou de formules.

Et oui, les énumérations ouvertes de Go ont des inconvénients, évidemment. Ils ont été mentionnés ci-dessus, en détail : vous pouvez oublier un cas dans un commutateur, ce qui entraîne des bogues. Ils n'ont pas d'espace de noms. Vous pouvez accidentellement utiliser une constante non symbolique qui finit par être une valeur invalide (ce qui entraîne des bogues).
Mais il me semble plus productif de parler de ces inconvénients et de les mesurer par rapport aux inconvénients de toute solution proposée, que de prendre une solution fixe (type enum) et d'argumenter sur ses compromis spécifiques pour résoudre les problèmes.

Pour moi, la plupart des inconvénients peuvent être principalement résolus de manière pragmatique dans le langage actuel, avec un outil linter détectant les déclarations const d'un certain type et vérifiant leurs utilisations. L'espacement des noms ne peut pas être résolu de cette façon, ce qui n'est pas génial. Mais il peut y avoir une solution différente à ce problème, que les énumérations aussi.

Je pourrais vous donner des "noms de registre" comme quelque chose qui n'est vraiment pas modifiable, mais en ce qui concerne les valeurs de protocole, je suis catégorique sur le fait que la position de les faire prendre des valeurs arbitraires pour l'extensibilité et la compatibilité est raisonnable. Encore une fois, proto2 -> proto3 contenait exactement ce changement et il l'a fait à partir de l'expérience acquise.

C'est pourquoi j'ai dit des valeurs établies. La base de format Fe wav n'a pas changé depuis de nombreuses années et offre une excellente capacité de retour. S'il s'agit de nouvelles valeurs, vous pouvez rester, utilisez des énumérations et ajoutez des valeurs.

Si vous tapez mal des noms, avoir des énumérations fermées ne vous aidera pas. Uniquement si vous n'utilisez pas les noms symboliques et utilisez plutôt int/string-literals.

Oui, cela ne m'aide pas à faire de bons noms, mais ils peuvent aider à organiser certaines valeurs avec un seul nom. Cela rend le processus de développement plus rapide dans certains cas. Il peut réduire le nombre de variantes avec saisie automatique à une.

C'est cependant le cas d'utilisation le plus courant présenté pour les énumérations. Exemple : ce numéro spécifique les utilise comme justification. Un autre cas souvent mentionné est celui des appels système - une architecture client-serveur déguisée. La généralisation de cet exemple est "tout code où deux ou plusieurs composants développés indépendamment échangent de telles valeurs", ce qui est incroyablement large, couvre la grande majorité des cas d'utilisation pour eux et, sous le modèle de réparation progressive, également toute API exportée .

Mais utiliser/ne pas utiliser des constantes/énumérations ne supprime pas le cœur du problème, vous devez toujours penser à la compatibilité arrière. Je veux dire que le problème n'est pas dans les enums/consts mais dans nos cas d'utilisation.

Personnellement, j'ai tendance à mettre fermement "paquet unique" sur la ligne de "pas un grand projet". Ainsi, je considère qu'il est beaucoup moins probable que vous oubliez un cas ou que vous changiez un emplacement de code, lors de l'extension d'une énumération.

Dans ce cas, vous avez toujours les avantages de la convection de nom et de la vérification du temps de compilation,

Toutes les expressions ne doivent pas être testées. Si c'est immédiatement évident, les tests sont exagérés. Si ce n'est pas le cas, notez les constantes, vous le feriez de toute façon dans un test.

Bien sûr, je comprends que toutes les lignes de code ne doivent pas être testées, mais si vous avez un précédent, vous devez le tester ou le réécrire. Je sais comment faire cela sans iota, mais mon vieil exemple n'est qu'une blague.

Encore une fois, vous ne pouvez pas utiliser cela comme argument en faveur des énumérations ; vous pouvez écrire les constantes comme vous le feriez dans une déclaration enum.

Ce n'est pas un argument pour les énumérations.

@Merovius

Si nous parlons d'énumérations fermées (ce dont les gens parlent généralement lorsqu'ils parlent d'énumérations) ou d'énumérations avec vérification de l'exhaustivité, alors elles sont certainement spéciales. Parce qu'ils ne sont pas extensibles.

Ils ne sont pas non plus extensibles en toute sécurité.

Si tu as

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

et

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

dans q , il est sûr d'utiliser D (à moins que la prochaine version de p n'ajoute sa propre étiquette pour Enum(3) ) mais seulement tant que vous n'avez jamais repassez-le à p : Vous pouvez prendre le résultat de p.Make et passer son état à D mais si vous appelez p.Take vous devez vous assurer que c'est n'étant pas passé q.D ET il doit s'assurer qu'il n'obtient qu'un des A , B , C ou vous avez un bogue. Vous pouvez contourner ce problème en faisant

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

Avec ou sans type fermé dans le langage, vous avez tous les problèmes d'avoir un type fermé mais sans que le compilateur ne vous surveille.

Votre exemple ne couvre que la limite de processus (en parlant d'erreurs de réseau), pas la limite de package. Comment les packages se comporteront-ils si j'ajoute un "InvalidInternalState" (pour inventer quelque chose) à FromTheNetwork ? Dois-je réparer leurs commutateurs avant qu'ils ne compilent à nouveau ? Ensuite, ce n'est pas extensible dans le modèle de réparation progressive. Ont-ils besoin d'un cas par défaut pour compiler en premier lieu ? Ensuite, il ne semble pas y avoir de raison d'énumérer.

Avec uniquement des types enum, vous devrez toujours faire comme ci-dessus et définir votre propre version avec les fonctions de conversion d'état et d'écriture supplémentaires.

Cependant, les types de somme sont composables même lorsqu'ils sont utilisés comme énumérations, vous pouvez donc en "étendre" un de manière naturelle et sûre de cette manière. J'ai donné un exemple, mais pour être plus explicite, étant donné

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

Enum peut être "étendu" avec

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

et cette fois, il est complètement sûr pour une nouvelle version de p d'ajouter D . Le seul inconvénient est que vous devez double-switcher pour accéder à l'état d'un p.Enum depuis l'intérieur d'un q.Enum mais c'est explicite et clair et, comme je l'ai mentionné, votre éditeur pourrait cracher le squelette des interrupteurs s'éteint automatiquement.

Mais pour cela, nous n'avons pas besoin d'énumérations réelles, en tant que types. Un tel outil de linting pourrait également vérifier de manière heuristique les déclarations const en utilisant iota, où chaque cas est d'un type donné et considérer qu'il s'agit d'un "énumération" et effectuer les vérifications souhaitées. Je serais complètement d'accord avec un outil utilisant ces "énumérations par convention" pour aider l'auto-complétion ou le linting que chaque commutateur doit avoir une valeur par défaut ou même que chaque cas (connu) doit être vérifié.

Il y a deux problèmes avec ceci.

Si vous avez un type intégral défini avec des étiquettes données à un sous-ensemble de son domaine via const/iota :

Premièrement, il peut représenter une énumération fermée ou ouverte. Bien qu'il soit principalement utilisé pour simuler un type fermé, il peut également être utilisé simplement pour donner des noms à des valeurs couramment utilisées. Considérez une énumération ouverte pour un format de fichier imaginaire :

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
)

Cela ne veut pas dire que 0, 1 et 42 sont le domaine du type Record. Le contrat est beaucoup plus subtil et nécessiterait la modélisation de types dépendants. (Ce serait certainement aller trop loin !)

Deuxièmement, nous pourrions supposer de manière heuristique qu'un type intégral défini avec des étiquettes constantes signifie que le domaine est restreint. Il obtiendrait un faux positif de ce qui précède, mais rien n'est parfait. Nous pourrions utiliser go/types pour extraire ce pseudo-type des définitions, puis parcourir et rechercher tous les commutateurs sur les valeurs de ce type et nous assurer qu'ils contiennent tous les étiquettes nécessaires. Cela peut être utile, mais nous n'avons pas montré d'exhaustivité à ce stade. Nous avons assuré la couverture de toutes les valeurs valides, mais nous n'avons pas prouvé qu'aucune valeur invalide n'a été créée. Cela n'est pas possible. Même si nous pouvions trouver chaque source, puits et transformation de valeurs et les interpréter de manière abstraite pour garantir statiquement qu'aucune valeur invalide n'a été créée, nous ne serions toujours pas en mesure de dire quoi que ce soit sur la valeur au moment de l'exécution, car reflect n'est pas conscient de la vraie domaine du type puisqu'il n'est pas encodé dans le système de type.

Il existe ici une alternative aux types enum et sum qui contourne ce problème, bien qu'elle ait ses propres problèmes.

Disons que le littéral de type range m n crée un type intégral qui est au moins m et au plus n (Pour tout v, m ≤ v ≤ n). Avec cela, nous pourrions limiter le domaine de l'énumération, comme

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

Étant donné que la taille du domaine = le nombre d'étiquettes, il est possible de savoir avec 100 % de confiance si une instruction switch épuise toutes les possibilités. Pour étendre cette énumération en externe, vous devez absolument créer des fonctions de conversion de type pour gérer le mappage, mais je maintiens toujours que vous devez le faire de toute façon.

Bien sûr, c'est en fait une famille de types étonnamment subtile à mettre en œuvre et qui ne fonctionnerait pas très bien avec le reste de Go. Il n'a pas non plus beaucoup d'utilisations en dehors de cela et certains cas d'utilisation de niche.

Nous pouvons ne pas être d'accord sur l'importance de maintenir une réparation et une extensibilité progressives - par exemple, beaucoup de gens pensent que le versionnage sémantique est une meilleure solution aux problèmes qu'il résout. Mais si vous les trouvez importants, il est parfaitement valide et raisonnable de considérer les énumérations comme nuisibles ou inutiles. Et c'était la question à laquelle je répondais : comment les gens peuvent raisonnablement faire le compromis d'exiger une vérification partout, au lieu de l'avoir dans le compilateur. Réponse : En valorisant l'extensibilité et l'évolution des API, ce qui rend ces vérifications nécessaires de toute façon sur le site d'utilisation.

Pour les types d'énumération de base, je suis d'accord. Au début de cette discussion, j'aurais simplement été mécontent s'ils avaient été choisis plutôt que des types de somme, mais maintenant je comprends pourquoi ils seraient nocifs. Merci à vous et à @griesemer d'avoir clarifié cela pour moi.

Pour les types de somme, je pense que ce que vous avez dit est une raison valable pour ne pas exiger que les commutateurs soient exhaustifs au moment de la compilation. Je pense toujours que les types fermés ont un certain nombre d'avantages et que des trois examinés ici, les types somme sont les plus flexibles sans les inconvénients des autres. Ils permettent de fermer un type sans inhiber l'extensibilité ou la réparation progressive tout en évitant les erreurs causées par des valeurs illégales, comme le fait tout bon type.

La principale raison pour laquelle j'utilise golang plutôt que python et javascript et d'autres langages courants sans type est la sécurité des types. J'ai fait beaucoup avec Java et une chose qui me manque dans golang que Java fournit est des énumérations sûres.

Je ne serais pas d'accord pour pouvoir différencier les types avec des énumérations. Si vous avez besoin d'ints, tenez-vous en à ints à la place, comme le fait Java. Si vous avez besoin d'énumérations sûres, je suggérerais de suivre la syntaxe.

type enums enum { foo, bar, baz }

@rudolfschmidt , je suis d'accord avec toi, ça pourrait ressembler à ça aussi :

type DaysOfTheWeek enum {
  Monday
  Tuesday
}

Mais il y a un petit écueil - nous devons être capables de prendre le contrôle de enum dans les cas où nous devons valider des données, les transformer en JSON ou interagir avec ou FS.
Si nous supposons aveuglément que enum est un ensemble d'entiers non signés, nous pouvons nous retrouver avec un iota.
Si nous voulons innover, nous devons penser à la commodité d'utilisation.
Par exemple, avec quelle facilité puis-je valider que la valeur à l'intérieur du JSON entrant est un élément valide de l'énumération ?
Et si je vous disais que le logiciel change ?

Disons que nous avons une liste de crypto-monnaies :

type CryptoCurrency enum {
  BTC
  ETH
  XMR
}

Nous échangeons des données avec plusieurs systèmes tiers. Disons que vous en avez des milliers.
Vous avez une longue histoire, nombre de données en stockage. Le temps passe, disons que BitCoin finit par mourir. Personne ne l'utilise.
Vous décidez donc de le supprimer de la structure :

type CryptoCurrency enum {
  ETH
  XMR
}

Cela entraîne une modification des données. Parce que toutes les valeurs de enum ont changé. C'est bon pour toi. Vous pouvez exécuter la migration de vos données. En ce qui concerne vos partenaires, certains d'entre eux ne bougent pas aussi vite, certains n'ont pas de ressources ou ne peuvent tout simplement pas le faire pour un certain nombre de raisons.
Mais vous avez ingéré des données d'eux. Vous finirez donc par avoir 2 énumérations : ancien et nouveau ; et un mappeur de données utilisant les deux.
Cela nous dit de donner une flexibilité de définition pour les énumérations et les capacités de valider et de rassembler/désorganiser ces types de données.

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
}

Nous devons réfléchir à l'applicabilité des énumérations et des cas d'utilisation.

Nous pouvons apprendre 2 nouveaux mots clés si cela peut nous faire économiser des milliers de lignes de code passe-partout.

Le terrain d'entente est en effet d'avoir la possibilité de valider les valeurs enum sans les restreindre dans ce que ces valeurs peuvent être. Le type Enum reste à peu près le même - c'est un tas de constantes nommées. La variable de type enum peut être égale à n'importe quelle valeur du type sous-jacent d'enum. Ce que vous ajoutez en plus de cela, c'est la possibilité de valider une valeur pour voir si elle contient une valeur d'énumération valide ou non. Et il pourrait y avoir d'autres bonus comme la stringification.

Très souvent, je suis dans une situation où j'ai un protocole (protobuf ou thrift) avec un tas d'énumérations partout. Je dois valider chacun d'eux et, si la valeur enum m'est inconnue, jeter ce message et signaler une erreur. Il n'y a pas d'autre moyen pour moi de gérer ce genre de message. Avec les langages où enum n'est qu'un tas de constantes, je n'ai pas d'autre moyen que d'écrire d'énormes quantités d'instructions switch vérifiant toutes les combinaisons possibles. C'est une grande quantité de code et des erreurs sont inévitables. Avec quelque chose comme C #, je peux utiliser le support intégré pour valider les énumérations, ce qui fait gagner beaucoup de temps. Certaines implémentations de protobuf le font en interne et lèvent une exception si c'est le cas. Sans parler de la simplicité de la journalisation - vous obtenez une stringification prête à l'emploi. C'est bien que protobuf génère une implémentation Stringer pour vous, mais tout dans votre code n'est pas protobuf.

Mais la possibilité de stocker n'importe quelle valeur est utile dans d'autres cas où vous ne voulez pas jeter des messages mais faire quelque chose avec eux même s'ils ne sont pas valides. Le client peut généralement jeter un message, mais côté serveur, vous devez souvent tout stocker dans la base de données. Jeter du contenu n'est pas une option là-bas.

Donc, pour moi, il y a une réelle valeur dans la capacité à valider les valeurs enum. Cela me ferait économiser des milliers de lignes de code passe-partout qui ne fait que valider.

Il me semble assez simple, de fournir cette fonctionnalité comme un outil. Une partie existe déjà dans le stringer-tool. S'il y a un outil que vous appelleriez comme enumer Foo , qui générerait des méthodes fmt.Stringer pour Foo et (disons) une méthode Known() bool qui vérifie si le la valeur stockée est dans la plage des valeurs connues, cela résoudrait-il vos problèmes ?

@Merovius en l'absence de quoi que ce soit d'autre, ce serait utile. Mais je suis généralement contre autogen. Le seul cas où cela est utile et fonctionne, ce sont des choses comme protobuf où vous avez un protocole assez stable qui peut être compilé une fois. L'utiliser pour les énumérations en général ressemble à une béquille pour un système de type simpliste. Et en regardant comment Go est une question de sécurité de type, cela va à l'encontre de la philosophie du langage lui-même. Au lieu d'aider la langue, vous commencez à développer cette infrastructure qui ne fait pas vraiment partie de l'écosystème linguistique. Laissez les outils externes pour la vérification, pas pour la mise en œuvre de ce qui manque au langage.

. L'utiliser pour les énumérations en général ressemble à une béquille pour un système de type simpliste.

Parce que c'est le cas - le système de type de Go est notoirement et intentionnellement simpliste. Mais ce n'était pas la question, la question était de savoir si cela réduirait vos problèmes. En dehors de "je n'aime pas ça", je ne vois pas vraiment comment ça ne marche pas (si vous supposez des énumérations ouvertes de toute façon).

Et en regardant comment Go est une question de sécurité de type, cela va à l'encontre de la philosophie du langage lui-même.

Go n'est pas "tout sur la sécurité du type". Des langages comme Idris sont tous axés sur la sécurité des types. Go concerne des problèmes d'ingénierie à grande échelle et, en tant que tel, sa conception est guidée par les problèmes qu'il tente de résoudre. Par exemple, son système de type permet de détecter un large éventail de bogues dus aux modifications de l'API et permet certaines refactorisations à grande échelle. Mais il est également intentionnellement simple, pour faciliter l'apprentissage, réduire la divergence des bases de code et augmenter la lisibilité du code tiers.

En tant que tel, si le cas d'utilisation qui vous intéresse (énumérations ouvertes) peut être résolu sans changement de langage, par un outil qui génère du code tout aussi facilement lisible, alors cela semble tout à fait conforme à la philosophie de Go. En particulier, l'ajout d'une nouvelle fonctionnalité de langue qui est un sous-ensemble de la fonctionnalité d'une fonctionnalité existante ne semble pas conforme à la conception de Go.

Donc, pour réitérer : il serait utile que vous puissiez expliquer comment l'utilisation d'un outil qui génère le passe-partout qui vous intéresse ne résout pas le problème réel - si rien d'autre, alors parce que comprendre cela est nécessaire pour informer la conception de la fonctionnalité de toute façon .

J'ai combiné certaines des idées de la discussion, qu'en pensez-vous ?

Quelques informations de base :

  1. Vous pouvez étendre une énumération comme tout autre type.
  2. Ils sont stockés comme une constante, mais avec le nom du type comme préfixe. Raison : lorsque vous utilisez les énumérations iota actuelles, vous écrivez probablement le nom de l'énumération comme préfixe de chaque constante. Avec cette fonctionnalité, vous pouvez simplement l'éviter.
  3. Ils sont immuables et traités comme toute autre constante.
  4. Vous pouvez parcourir les énumérations. Lorsque vous faites cela, ils se comportent comme une carte. La clé est le nom de l'énumération, la valeur est la valeur de l'énumération.
  5. Vous pouvez ajouter des méthodes à une énumération, comme vous le faites pour tout autre type.
  6. Chaque valeur d'énumération doit générer automatiquement des méthodes :
  7. Name() renverra le nom de la variable enum
  8. Index() renverra l'index enum, qui augmente automatiquement. Il commence là où commence un tableau.

Code:
``` allez
paquet principal

//Exemple A
type Country enum[struct] { //les énumérations peuvent étendre d'autres types (regardez l'exemple B)
Austria("AT", "Austria", false) //Sera accessible comme un const, mais avec le type as
Allemagne("DE", "Allemagne", vrai) //préfixe (par exemple Pays.Autriche)

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

}

// Les énumérations peuvent avoir des méthodes comme tout autre type
func (c Pays) test() {}

fonction principale() {
println(Pays.Autriche.NomPays) //Autriche
println(Pays.Allemagne.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
}

}

//Exemple B
type Fraîcheur enum[int] {
Très cool(10)
Frais(5)
Pas cool(0)
}```

@sinnlosername Je pense que les énumérations devraient être quelque chose de très facile à comprendre. La combinaison de certaines des idées présentées dans la discussion précédente ne conduit pas nécessairement à la meilleure idée d'énumération.

Je crois que ce qui suit serait simple à comprendre :

Déclaration

type Day enum {
    Monday
    Tuesday
    ...
    Sunday
}

Conversion de chaîne (à l'aide de l'interface Stringer ) :

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

C'est si simple. L'avantage de cela est de permettre une sécurité de type plus forte lors du passage des énumérations.

Exemple d'utilisation

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

Si je devais utiliser des constantes string ici pour représenter un Day , IsWeekday dirait que toute chaîne qui n'est pas "sat" ou "sun" est un jour de la semaine (c'est-à-dire, qu'est-ce que IsWeekday("abc") retournerait ?). En revanche, le domaine de la fonction ci-dessus est restreint, permettant ainsi à la fonction d'avoir plus de sens par rapport à son entrée.

@ljeabmreosn

ça devrait probablement être

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

J'ai renoncé à attendre que l'équipe golang améliore la langue de manière nécessaire. Je peux tout le monde recommander de jeter un coup d'œil à rust lang, il a déjà toutes les fonctionnalités souhaitées comme enum et génériques et plus encore.

Nous sommes en 14 mai 2018 et nous discutons toujours du support enum. Je veux dire quoi diable? Personnellement je suis déçu de golang.

Je peux comprendre que cela puisse devenir frustrant en attendant une fonctionnalité. Mais publier des commentaires non constructifs comme celui-ci n'aide pas. Veuillez garder vos commentaires respectueux. Voir https://golang.org/conduct.

Merci.

@agnivade Je suis d'accord avec @rudolfschmidt. GoLang n'est certainement pas non plus mon langage préféré à cause de ce manque de fonctionnalités, d'API et de cette trop grande résistance au changement ou à l'acceptation des erreurs passées par les créateurs de Go. Mais je n'ai pas le choix en ce moment car je n'étais pas décideur de la langue à choisir pour mon dernier projet sur mon lieu de travail. Je dois donc travailler avec toutes ses lacunes. Mais pour être honnête, c'est comme une torture d'écrire des codes en GoLang ;-)

J'ai renoncé à attendre que l'équipe golang améliore la langue de manière nécessaire.

  • Le mot nécessaire ne signifie pas "ce que je veux".

En fait, les fonctionnalités de base de chaque langue moderne sont nécessaires. GoLang a quelques bonnes fonctionnalités, mais il ne survivra pas si le projet reste conservateur. Les fonctionnalités telles que les énumérations ou les génériques n'ont aucun inconvénient pour les personnes qui ne les aiment pas, mais elles présentent de nombreux avantages pour les personnes qui souhaitent les utiliser.

Et ne me dites pas "mais allez veut rester simple". Il y a une énorme différence entre « simple » et « sans fonctionnalités réelles ». Java est très simple mais il manque beaucoup de fonctionnalités. Donc, soit les développeurs Java sont des assistants, soit cet argument est tout simplement mauvais.

En fait, les fonctionnalités de base de chaque langue moderne sont nécessaires.

Sûr. Ces caractéristiques de base sont appelées complétude de Turing. _Tout_ le reste est un choix de conception. Il y a beaucoup d'espace entre la complétude de Turing et C++ (par exemple) et dans cet espace, vous pouvez trouver beaucoup de langages. La distribution suggère qu'il n'existe pas d'optimum global.

GoLang a quelques bonnes fonctionnalités, mais il ne survivra pas si le projet reste conservateur.

Peut-être. Jusqu'à présent, il continue de croître. IMO, il ne serait pas encore en croissance s'il n'avait pas été conservateur. Nos deux opinions sont subjectives et ne valent techniquement pas grand-chose. C'est l'expérience et le goût des créateurs qui priment. C'est bien d'avoir une opinion différente, mais cela ne garantit pas que les concepteurs la partageront.

BTW, si j'imagine ce que serait Go aujourd'hui si 10% des fonctionnalités demandées par les gens étaient adoptées, je n'utiliserais probablement plus Go maintenant.

En fait, vous venez de manquer l'argument le plus important de ma réponse. Peut-être parce que c'est déjà un contresens à certaines des choses que vous avez dites.

"Les fonctionnalités telles que les énumérations ou les génériques n'ont aucun inconvénient pour les personnes qui ne les aiment pas, mais elles présentent de nombreux avantages pour les personnes qui souhaitent les utiliser."

Et pourquoi pensez-vous que cette attitude conservatrice est une raison de la croissance de golang ? Je pense que cela est plus probablement lié à l'efficacité de golang et au grand ensemble de bibliothèques standard.

Java a également connu une sorte de "crash" en essayant de changer des choses importantes dans Java 9, ce qui a probablement poussé de nombreuses personnes à rechercher une alternative. Mais regardez Java avant ce crash. Il était en croissance constante car il comportait de plus en plus de fonctionnalités qui facilitaient la vie des développeurs.

"Les fonctionnalités telles que les énumérations ou les génériques n'ont aucun inconvénient pour les personnes qui ne les aiment pas, mais elles présentent de nombreux avantages pour les personnes qui souhaitent les utiliser."

Ce n'est très clairement pas vrai. Chaque fonctionnalité finira par arriver dans la stdlib et/ou les packages que je souhaite importer. _Tout le monde_ devra gérer les nouvelles fonctionnalités, qu'elles les aiment ou non.

Jusqu'à présent, il continue de croître. IMO, il ne serait pas encore en croissance s'il n'avait pas été conservateur

Je ne pense pas que sa croissance lente (le cas échéant) soit due à la prudence, mais plutôt à une bibliothèque standard, à un ensemble déjà existant de fonctionnalités de langage, à des outils. C'est ce qui m'a amené ici. L'ajout de fonctionnalités linguistiques ne changerait rien pour moi à cet égard.

Si nous regardons C# et Typescript ou même Rust/Swift. Ils ajoutent de nouvelles fonctionnalités comme un fou. C # est toujours dans les principaux langages fluctuant de haut en bas. Typescript se développe très rapidement. Idem pour Rust/Swift. Go, d'autre part, a explosé en popularité en 2009 et 2016. Mais entre cela, il n'a pas du tout augmenté et a en fait perdu. Go n'a rien à donner aux nouveaux développeurs s'ils le savaient déjà et ne l'ont pas choisi plus tôt pour une raison quelconque. Exactement parce que Go stagne dans sa conception. D'autres langues ajoutent des fonctionnalités non pas parce qu'elles n'ont rien d'autre à faire, mais parce que la base d'utilisateurs réelle l'exige. Les gens ont besoin de nouvelles fonctionnalités pour que leur base de code reste pertinente dans des domaines problématiques en constante évolution. Comme asynchrone/attendre. Il était nécessaire de résoudre un problème réel. Pas étonnant que vous puissiez le voir dans de nombreuses langues maintenant.

Il y aura éventuellement Go 2 et vous pouvez être absolument sûr qu'il apportera de nombreux nouveaux développeurs. Non pas parce qu'il est nouveau et brillant, mais parce que de nouvelles fonctionnalités pourraient convaincre quelqu'un de finalement changer ou de l'essayer. Si la prudence était si importante, nous aurions même ces propositions.

Je ne pense pas que sa croissance lente (le cas échéant) soit due à la prudence, mais plutôt à une bibliothèque standard, à un ensemble déjà existant de fonctionnalités de langage, à des outils. C'est ce qui m'a amené ici.

Et c'est le résultat d'être conservateur. Si la langue casse quelque chose/tout tous les [semi] ans environ, vous n'aurez rien de ce que vous dites apprécier à propos de Go parce qu'il y aura beaucoup moins de gens qui vous apporteront cela.

L'ajout de fonctionnalités linguistiques ne changerait rien pour moi à cet égard.

Êtes-vous sûr de cela? Voir au dessus.


BTW, avez-vous vu les résultats de l'enquête 2017 ?

Si la langue casse quelque chose/tout tous les [semi]ans environ

Alors ne cassez rien. C # a ajouté une tonne de fonctionnalités et n'a jamais violé la rétrocompatibilité. Ce n'est pas non plus une option pour eux. Pareil pour C++ je pense. Si Go ne peut pas ajouter de fonctionnalité sans casser quelque chose, alors c'est un problème avec Go et, éventuellement, comment il est implémenté.

BTW, avez-vous vu les résultats de l'enquête 2017 ?

Mon commentaire est basé sur les enquêtes 2017/2018, l'index TIOBE et mes observations générales sur ce qui se passe avec différentes langues.

@cznic
Tout le monde doit y faire face, mais vous n'avez pas besoin de les utiliser. Si vous préférez écrire votre code avec des énumérations et des cartes iota, vous pouvez toujours le faire. Et si vous n'aimez pas les génériques, utilisez des bibliothèques sans eux. Java prouve qu'il est possible d'avoir les deux.

Alors ne cassez rien.

Bonne idée. Cependant, beaucoup, sinon la plupart des changements proposés au langage, _y compris celui-ci_, sont des changements de rupture.

C # a ajouté une tonne de fonctionnalités et n'a jamais violé la rétrocompatibilité.

Veuillez vérifier vos faits : Visual C# 2010 Breaking Changes . (premier résultat de recherche sur le Web, je ne peux que deviner s'il s'agit ou non du seul exemple.)

Mon commentaire est basé sur les enquêtes 2017/2018, l'index TIOBE et mes observations générales sur ce qui se passe avec différentes langues.

Eh bien, comment pouvez-vous alors voir que la langue ne se développe pas alors que les résultats de l'enquête montrent une croissance de 70 % d'une année sur l'autre du nombre de répondants ?

Comment définiriez-vous « changements avec rupture » ? Chaque ligne de code go fonctionnerait toujours après l'ajout d'énumérations ou de génériques.

Tout le monde doit y faire face, mais vous n'avez pas besoin de les utiliser. Si vous préférez écrire votre code avec des énumérations et des cartes iota, vous pouvez toujours le faire. Et si vous n'aimez pas les génériques, utilisez des bibliothèques sans eux. Java prouve qu'il est possible d'avoir les deux.

Je ne peux pas être d'accord. Une fois que le langage devient, par exemple des génériques, même si je ne les utilise pas, ils seront utilisés partout. Même en interne sans changer l'API. Le résultat est que je suis très affecté par eux, car il n'y a certainement aucun moyen d'ajouter des génériques au langage sans ralentir la construction des programmes qui les utilisent. Aka "Pas de déjeuner gratuit".

Comment définiriez-vous « changements avec rupture » ? Chaque ligne de code go fonctionnerait toujours après l'ajout d'énumérations ou de génériques.

Bien sûr que non. Ce code ne compilerait plus avec cette proposition :

package foo

var enum = 42

Le mot nécessaire ne signifie pas "ce que je veux".

bien sûr, cela ne veut pas dire, et je ne l'ai jamais pensé. Bien sûr, vous pouvez répondre que de telles fonctionnalités ne sont pas nécessaires, mais je peux alors répondre à ce qui est nécessaire en général. Rien n'est nécessaire et nous pouvons revenir au stylo et au papier.

Golang prétend être une langue pour les grandes équipes. Je ne sais pas si vous pouvez utiliser golang pour développer de grandes bases de code. Pour cela, vous avez besoin d'une compilation statique et d'une vérification de type pour éviter autant que possible les erreurs d'exécution. Comment pouvez-vous le faire sans énumérations et génériques ? Ces fonctionnalités ne sont même pas fantaisistes ou agréables à avoir, mais sont absolument essentielles pour un développement sérieux. Si vous ne les avez pas, vous finissez par utiliser des interfaces{} partout. Quel est l'intérêt d'avoir des types de données si vous êtes obligé d'utiliser des interfaces{} dans votre code ?

Bien sûr, si vous n'avez pas le choix, vous le ferez aussi, mais pourquoi devriez-vous si vous avez des alternatives comme la rouille qui offrent déjà tout cela et qui sont encore plus rapides à exécuter que Golang ne peut jamais l'être ? Je me demande vraiment si go a un avenir avec cet état d'esprit de :

Le mot nécessaire ne signifie pas "ce que je veux".

Je respecte toute contribution à l'open source et si golang est un projet de passe-temps, c'est bien comme ça, mais golang veut être pris au sérieux et pour le moment c'est plus un jouet pour certains développeurs ennuyés et je ne vois pas de volonté de changer ça.

L'API n'a pas besoin d'être modifiée, seules les nouvelles parties d'API peuvent utiliser des génériques, mais il existe probablement toujours des alternatives sans génériques sur Internet.

Et les deux, une compilation un peu plus lente et des variables appelées "enum" sont des effets minimes. En fait, 99 % des gens ne le remarqueront même pas et les 1 % restants n'auront qu'à ajouter quelques petits changements tolérables. Ce n'est pas comparable, par exemple, au puzzle de Java qui fout tout en l'air.

Et les deux, une compilation un peu plus lente et des variables appelées "enum" sont des effets minimes. En fait, 99 % des gens ne le remarqueront même pas et les 1 % restants n'auront qu'à ajouter quelques petits changements tolérables.

Tout le monde serait heureux si quelqu'un pouvait proposer une conception et une mise en œuvre offrant des performances aussi merveilleuses. Merci de contribuer au #15292.

Cependant, s'il s'agit d'un jeu nommé "tirer n'importe quel nombre en ma faveur sans aucune donnée de support", désolé, mais je ne participe pas.

Avez-vous des chiffres sur la différence de vitesse avec les génériques ?

Et oui, ces chiffres ne sont étayés par aucune donnée, car ils indiquent simplement que la probabilité d'avoir des variables appelées "enum" n'est pas très élevée.

Je voudrais rappeler à tout le monde que de nombreuses personnes se sont abonnées à ce numéro pour la question spécifique de savoir si et comment des énumérations pourraient être ajoutées à Go. Les questions générales du "Go est-il un bon langage ?" et "Est-ce que Go devrait se concentrer davantage sur la fourniture de fonctionnalités ?" sont probablement mieux discutés dans un forum différent.

Avez-vous des chiffres sur la différence de vitesse avec les génériques ?

Non, c'est pourquoi je n'en ai pas posté. J'ai seulement posté que le coût ne peut pas être nul.

Et oui, ces chiffres ne sont étayés par aucune donnée, car ils indiquent simplement que la probabilité d'avoir des variables appelées "enum" n'est pas très élevée.

C'est mélangé. Le ralentissement concernait les génériques. "enum" concernait la rétrocompatibilité et votre faux "_Every_ ligne de code go fonctionnerait toujours après l'ajout d'énumérations ou de génériques". réclamer. (souligne le mien)

@Merovius Vous avez raison, je me tais maintenant.

En ramenant cela aux types enum, qui est l'objet de ce problème, je comprends parfaitement l'argument pour lequel Go a besoin de génériques, mais je suis beaucoup plus fragile sur l'argument pour lequel Go a besoin de types enum. En fait, j'ai posé la question ci-dessus dans https://github.com/golang/go/issues/19814#issuecomment -290878151 et je tremble encore dessus. S'il y avait une bonne réponse à cela, je l'ai ratée. Quelqu'un pourrait-il le répéter ou le signaler ? Merci.

@ianlancetaylor Je ne pense pas que le cas d'utilisation soit compliqué, voulant un moyen sûr de type pour s'assurer qu'une valeur appartient à un ensemble de valeurs prédéfini, ce qui n'est pas possible aujourd'hui en Go. La seule solution de contournement consiste à valider manuellement à chaque point d'entrée possible dans votre code, y compris les RPC et les appels de fonction, ce qui est par nature peu fiable. Les autres subtilités syntaxiques pour l'itération facilitent de nombreux cas d'utilisation courants. Que vous trouviez ou non cette valeur est subjectif, et évidemment aucun des arguments ci-dessus n'a été convaincant pour les pouvoirs en place, donc j'ai essentiellement renoncé à ce que cela soit abordé au niveau du langage.

@ianlancetaylor : tout a à voir avec la sécurité des types. vous utilisez des types pour minimiser le risque d'erreurs d'exécution en raison d'une faute de frappe ou de l'utilisation de types incompatibles. Pour le moment, vous pouvez écrire en go

if enumReference == 1

car pour le moment, les énumérations ne sont que des nombres ou d'autres types de données primitifs.

Ce code ne devrait pas être possible du tout et devrait être évité. La même discussion que vous avez eue dans la communauté Java il y a des années, c'est la raison pour laquelle ils ont introduit les énumérations parce qu'ils en ont compris l'importance.

Vous ne devriez pouvoir écrire que

if enumReference == enumType

vous n'avez pas besoin de trop de fantaisie pour imaginer dans quels scénarios if enumReference == 1 peuvent se produire de manière plus cachée et conduire à des problèmes supplémentaires que vous ne verrez qu'à l'exécution.

Je veux juste mentionner : Go a son potentiel, mais il est étrange que des choses et des concepts éprouvés et compris depuis des années soient discutés ici comme vous discutez de nouveaux concepts ou paradigmes de programmation. Si vous avez un autre moyen d'assurer la sécurité des types, il existe peut-être quelque chose de mieux que les énumérations, mais je ne le vois pas.

Go a son potentiel, mais il est étrange que des choses et des concepts éprouvés et compris depuis des années soient discutés ici comme vous discutez de nouveaux concepts ou paradigmes de programmation.

Afais, surtout en suivant les autres discussions sur les génériques, les types de sommes, etc., il ne s'agit pas tant de savoir s'il faut l'avoir, mais comment l'implémenter. Le système de type Javas est extrêmement extensible et bien spécifié. C'est une énorme différence.

Dans Go, les gens essaient de trouver des moyens d'ajouter des fonctionnalités au langage, sans augmenter la complexité des compilateurs. Cela ne fonctionne généralement pas trop bien et les fait abandonner ces idées initiales.

Même si je pense moi aussi que ces priorités sont plutôt absurdes dans leur forme et leur qualité actuelles, votre meilleure chance est de proposer la mise en œuvre la plus simple possible et la moins perturbatrice . Rien d'autre ne vous mènera plus loin, imo.

@derekperkins @rudolfschmidt Merci. Je tiens à préciser que bien que C++ ait des types enum, les fonctionnalités que vous suggérez ne sont pas en C++. Il n'y a donc rien d'évident à ce sujet.,

En général, si une variable d'un type enum ne peut accepter que des valeurs de cet enum, cela serait inutile. En particulier, il doit y avoir une conversion d'un entier arbitraire vers le type enum ; sinon, vous ne pouvez pas envoyer d'énumérations via une connexion réseau. Eh bien, vous pouvez, mais vous devez écrire un commutateur avec un cas pour chaque valeur enum, ce qui semble vraiment fastidieux. Ainsi, lors d'une conversion, le compilateur génère-t-il une vérification que la valeur est une valeur d'énumération valide lors de la conversion de type ? Et est-ce que ça panique si la valeur n'est pas valide ?

Les valeurs enum doivent-elles être séquentielles ou peuvent-elles prendre n'importe quelle valeur comme en C++ ?

En Go, les constantes ne sont pas typées, donc si nous autorisons les conversions d'entiers vers un type enum, il serait étrange d'interdire if enumVal == 1 . Mais je suppose que nous pourrions.

L'un des principes généraux de conception de Go est que les personnes qui écrivent Go écrivent du code, pas des types. Je ne vois pas encore d'avantage provenant des types enum qui nous aide lors de l'écriture de code. Ils semblent ajouter un ensemble de contraintes de type d'un type que nous n'avons généralement pas en Go. Pour le meilleur ou pour le pire, Go ne fournit pas de mécanismes pour contrôler la valeur des types. Je dois donc dire que pour moi, l'argument en faveur de l'ajout d'énumérations à Go ne semble pas encore convaincant.

Je vais me répéter mais je suis favorable au maintien des énumérations telles qu'elles sont aujourd'hui et à l'ajout de fonctionnalités par-dessus :

  • le type enum a un type de valeur sous-jacent et quelques constantes nommées qui lui sont associées
  • Le compilateur doit autoriser la conversion d'une valeur arbitraire en valeur enum tant que leurs types sous-jacents sont compatibles. Toute valeur de type int doit être convertible en n'importe quel type d'énumération entière.
  • la conversion qui conduit à une valeur d'énumération non valide est autorisée. Le type Enum ne doit pas imposer de contraintes sur les valeurs qu'une variable peut prendre.

Ce qu'il propose en plus c'est :

  • stringification des valeurs enum. D'après mon expérience, très utile pour l'interface utilisateur et la journalisation. Si la valeur enum est valide, la stringification renvoie le nom de la constante. S'il n'est pas valide, il renvoie une représentation sous forme de chaîne de la valeur sous-jacente. Si -1 n'est pas une valeur d'énumération valide d'un type d'énumération Foo , la chaîne doit simplement renvoyer -1 .
  • permettre au développeur de déterminer si la valeur est une valeur d'énumération valide au moment de l'exécution. Très utile lorsque vous travaillez avec n'importe quel type de protocole. Au fur et à mesure que les protocoles évoluent, de nouvelles valeurs d'énumération pourraient être introduites dont le programme n'a pas connaissance. Ou cela pourrait être une simple erreur. À l'heure actuelle, vous devez soit vous assurer que les valeurs enum sont strictement séquentielles (pas quelque chose que vous pouvez toujours appliquer), soit vérifier manuellement toutes les valeurs possibles. Ce type de code devient très gros très rapidement et des erreurs sont inévitables.
  • éventuellement permettre au développeur d'énumérer toutes les valeurs possibles d'un type enum. J'ai vu des gens demander cela ici, d'autres langues l'ont aussi, mais je ne me souviens pas en avoir eu besoin moi-même, donc je n'ai aucune expérience personnelle en faveur de cela.

Ma justification est d'écrire du code et d'éviter les bogues. Toutes ces tâches sont fastidieuses et inutiles pour le développeur à faire à la main ou même à introduire des outils externes compliquant le code et la construction de scripts. Ces fonctionnalités couvrent tout ce dont j'ai besoin des énumérations sans trop les compliquer et les restreindre. Je ne pense pas que Go ait besoin de quelque chose comme des énumérations dans Swift ou même Java.


Il y a eu des discussions sur la validation au moment de la compilation que l'instruction switch couvre toutes les valeurs d'énumération possibles. Avec ma proposition, ce sera inutile. Les vérifications d'épuisement ne couvriront pas les valeurs d'énumération non valides, vous devez donc toujours avoir une casse par défaut pour les gérer. Cela est nécessaire pour prendre en charge la réparation progressive du code. La seule chose que nous pouvons faire ici, je pense, est de produire un avertissement si l'instruction switch n'a pas de cas par défaut. Mais cela peut être fait même sans changer de langue.

@ianlancetaylor Je pense que votre argument a quelques défauts.

En général, si une variable d'un type enum ne peut accepter que des valeurs de cet enum, cela serait inutile. En particulier, il doit y avoir une conversion d'un entier arbitraire vers le type enum ; sinon, vous ne pouvez pas envoyer d'énumérations via une connexion réseau.

L'abstraction pour le programmeur est bonne ; Go fournit de nombreuses abstractions. Par exemple, le code suivant ne compile pas :

package main

import "fmt"

const NULL = 0x0

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

mais dans C , un programme de ce style compilerait. C'est parce que Go est fortement typé et que C ne l'est pas.

Les indices d'énumérations peuvent être stockés en interne mais être cachés à l'utilisateur en tant qu'abstraction, semblable aux adresses de variables.

@zerkms Oui, c'est une possibilité, mais étant donné le type de d , l'inférence de type devrait être possible; cependant, l'utilisation qualifiée des énumérations (comme dans votre exemple) est un peu plus facile à lire.

@ianlancetaylor c'est une version très C des énumérations dont vous parlez. Je suis sûr que beaucoup de gens aimeraient ça mais, imo:

Les valeurs d'énumération ne doivent pas avoir de propriétés numériques. Les valeurs de chaque type d'énumération doivent être leur propre univers fini d'étiquettes discrètes, appliquées au moment de la compilation, sans rapport avec des nombres ou d'autres types d'énumération. La seule chose que vous pouvez faire avec une paire de ces valeurs est == ou != . D'autres opérations peuvent être définies comme des méthodes ou avec des fonctions.

L'implémentation va compiler ces valeurs en nombres entiers, mais ce n'est pas une chose fondamentale avec une raison légitime d'être exposée directement au programmeur, sauf par risque ou par réflexion. Pour la même raison que vous ne pouvez pas faire bool(0) pour obtenir false .

Si vous souhaitez convertir une énumération vers ou à partir d'un nombre ou de tout autre type, vous écrivez tous les cas et incluez la gestion des erreurs appropriée à la situation. Si c'est fastidieux, vous utilisez un générateur de code comme stringer ou au moins quelque chose pour remplir les cases dans l'instruction switch.

Si vous envoyez la valeur hors processus, un int est bon si vous suivez une norme bien définie ou si vous savez que vous parlez à une autre instance de votre programme qui a été compilée à partir de la source ou si vous devez faire des choses tenir dans le plus petit espace possible même si cela peut causer des problèmes, mais généralement aucun de ceux-ci ne tient et il est préférable d'utiliser une représentation sous forme de chaîne afin que la valeur ne soit pas affectée par l'ordre source de la définition de type. Vous ne voulez pas que le vert du processus A devienne le bleu du processus B parce que quelqu'un d'autre a décidé que le bleu devrait être ajouté avant le vert pour conserver l'ordre alphabétique dans la définition : vous voulez unrecognized color "Blue" .

C'est un bon moyen sûr de représenter un certain nombre d'états de manière abstraite. Il laisse le programme définir ce que signifient ces états.

(Bien sûr, vous souhaitez souvent associer des données à ces états et le type de ces données varie d'un état à l'autre...)

@ljeabmreosn Mon point était que si Go permet la conversion d'entiers en types enum, il serait naturel qu'une constante non typée se convertisse automatiquement en un type enum. Votre contre-exemple est différent, car Go ne permet pas de convertir des entiers en types pointeurs.

@jimmyfrasche Si vous devez écrire un commutateur pour convertir entre les entiers et les types enum, alors je suis d'accord que cela fonctionnerait proprement dans Go, mais franchement, cela ne semble pas suffisamment utile pour être ajouté au langage par lui-même. Cela devient un cas particulier des types somme, pour lesquels voir #19412.

Il y a beaucoup de propositions ici.

Un commentaire général : pour toute proposition qui n'expose pas une valeur sous-jacente (par exemple, un int) que vous pouvez convertir vers et depuis une énumération, voici quelques questions auxquelles répondre.

Quelle est la valeur zéro d'un type enum ?

Comment passe-t-on d'une énumération à une autre ? Je soupçonne que pour beaucoup de gens, les jours de la semaine sont un exemple canonique d'énumération, mais on pourrait raisonnablement vouloir "incrémenter" du mercredi au jeudi. Je ne voudrais pas avoir à écrire une grosse déclaration de commutateur pour cela.

(En outre, en ce qui concerne la "stringification", la chaîne correcte pour un jour de la semaine dépend de la langue et des paramètres régionaux.)

@josharian stringification signifie généralement convertir automatiquement les noms des valeurs enum en chaînes par le compilateur. Pas de localisation ou quoi que ce soit. Si vous voulez construire quelque chose en plus de cela, comme la localisation, alors vous le faites par d'autres moyens et d'autres langages fournissent un langage riche et des outils de cadre pour le faire.

Par exemple, certains types C# ont un remplacement ToString qui prend également des informations sur la culture. Ou vous pouvez utiliser l'objet DateTime lui-même et utiliser sa méthode ToString qui accepte à la fois les informations de format et de culture. Mais ces remplacements ne sont pas standard, la classe object dont tout le monde hérite n'a que ToString() . À peu près comme l'interface stringer dans Go.

Je pense donc que la localisation devrait être en dehors de cette proposition et des énumérations en général. Si vous voulez l'implémenter, faites-le d'une autre manière. Comme l'interface de stringer personnalisée, par exemple.

@josharian Puisque, du point de vue de l'implémentation, il s'agirait toujours d'un int et que les valeurs zéro seraient toutes des bits nuls, la valeur zéro serait la première valeur dans l'ordre des sources. C'est une sorte de fuite de l'int-iness mais en fait assez agréable car vous pouvez choisir la valeur zéro, décider si une semaine commence le lundi ou le dimanche, par exemple. Bien sûr, c'est moins sympa que l'ordre des termes restants n'ait pas un tel impact et que réordonner les valeurs puisse avoir des impacts non triviaux si vous changez le premier élément. Ce n'est pas vraiment différent de const/iota, cependant.

Re stringification ce que @creker a dit. Pour développer, cependant, je m'attendrais à

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

pour imprimer dimanche non 0. L'étiquette est la valeur, pas sa représentation.

Pour être clair, je ne dis pas qu'il devrait avoir une méthode String implicite, juste que les étiquettes soient stockées dans le cadre du type et accessibles par réflexion. (Peut-être que Println appelle Label() sur un reflect.Value d'un enum ou quelque chose comme ça ? Je n'ai pas approfondi la façon dont fmt fait son vaudou.)

Comment passe-t-on d'une énumération à une autre ? Je soupçonne que pour beaucoup de gens, les jours de la semaine sont un exemple canonique d'énumération, mais on pourrait raisonnablement vouloir "incrémenter" du mercredi au jeudi. Je ne voudrais pas avoir à écrire une grosse déclaration de commutateur pour cela.

Je pense que la réflexion ou un gros interrupteur est la bonne chose. Les modèles communs peuvent facilement être remplis avec go generate pour créer des méthodes sur le type ou des fonctions d'usine de ce type (et peut-être même reconnus par le compilateur pour le réduire à l'arithmétique sur la représentation).

Cela n'a pas de sens pour moi de supposer que toutes les énumérations ont un ordre total ou qu'elles sont cycliques. Étant donné type failure enum { none; input; file; network } , est-il vraiment logique d'imposer qu'une entrée non valide est inférieure à une défaillance de fichier ou que l'incrémentation d'une défaillance de fichier entraîne une défaillance du réseau ou que l'incrémentation d'une défaillance du réseau aboutit au succès ?

En supposant que l'utilisation principale concerne les valeurs ordonnées cycliques, une autre façon de gérer cela serait de créer une nouvelle classe de types entiers paramétrés. C'est une mauvaise syntaxe, mais, pour la discussion, disons que c'est I%NI est dans un type entier et N est une constante entière. Toute arithmétique avec une valeur de ce type est implicitement mod N. Alors vous pourriez faire

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

donc samedi + 1 == dimanche et jour de la semaine (456) == lundi. Il est impossible de construire un jour de semaine invalide. Cela pourrait être utile en dehors de const/iota, cependant.

Pour quand vous ne voulez pas du tout que ce soit un nombre-y, comme @ianlancetaylor l' a souligné, ce que je veux vraiment, ce sont des types de somme.

L'introduction d'un type arithmétique modulaire arbitraire est une suggestion intéressante. Ensuite, les énumérations pourraient être de cette forme, ce qui vous donne une méthode String triviale :

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

type Weekday = uint % len(Weekdays)

Combiné avec des entiers de taille arbitraire, cela vous donne également int128, int256, etc.

Vous pouvez également définir des éléments intégrés :

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

Le compilateur peut prouver plus de bornes qu'auparavant. Et les API peuvent fournir des assertions plus précises via des types, par exemple la fonction Len64 dans math/bits peut maintenant retourner uint % 64 .

Lorsque je travaillais sur un port RISC-V, je voulais un type uint12 , puisque mes composants d'encodage d'instructions sont de 12 bits ; cela aurait pu être uint % (1<<12) . De nombreuses manipulations de bits, en particulier des protocoles, pourraient en bénéficier.

Les inconvénients sont importants, bien sûr. Go a tendance à favoriser le code plutôt que les types, et c'est lourd en type. Des opérations comme + et - peuvent soudainement devenir aussi coûteuses que %. Sans paramétricité de type quelconque, vous devrez probablement convertir en canonique uint8 , uint16 , etc. afin d'interagir avec presque toutes les fonctions de la bibliothèque, et la conversion peut masquer les limites échecs (à moins que nous ayons un moyen de faire une conversion de panique hors plage, ce qui introduit sa propre complexité). Et je peux voir qu'il est surutilisé, par exemple en utilisant uint % 1000 pour les codes d'état HTTP.

Une idée intéressante néanmoins. :)


Autres petites réponses :

C'est une sorte de fuite de l'int-iness

Cela me fait penser qu'ils sont vraiment int. :)

Les modèles courants peuvent facilement être remplis avec go generate

Si vous devez de toute façon générer du code avec des énumérations, il me semble que vous pouvez également générer des fonctions de chaîne et des vérifications de limites, etc., et effectuer des énumérations avec la génération de code au lieu du poids de la prise en charge du langage.

Cela n'a pas de sens pour moi de supposer que toutes les énumérations ont un ordre total ou qu'elles sont cycliques.

Assez juste. Cela me fait penser qu'avoir une poignée de cas d'utilisation concrets aiderait à clarifier exactement ce que nous voulons des énumérations. Je soupçonne en quelque sorte qu'il n'y aura pas d'ensemble clair d'exigences et que l'émulation d'énumérations utilisant d'autres constructions de langage (c'est-à-dire le statu quo) finira par avoir le plus de sens. Mais ce n'est qu'une hypothèse.

Re stringification ce que @creker a dit.

Assez juste. Mais je me demande combien de cas finissent par ressembler à des jours de la semaine. Tout ce qui concerne l'utilisateur, bien sûr. Et la stringification semble être l'une des principales demandes d'énumérations.

@josharian Enums qui sont vraiment des entiers auraient probablement besoin d'un mécanisme similaire. Sinon, qu'est-ce que enum { A; B; C}(42) ?

Vous pouvez dire que c'est une erreur du compilateur, mais cela ne fonctionne pas dans un code plus compliqué car vous pouvez convertir vers et depuis des entiers au moment de l'exécution.

C'est soit A, soit une panique d'exécution. Dans les deux cas, vous ajoutez un type intégral avec un domaine limité. S'il s'agit d'une panique d'exécution, vous ajoutez un type intégral qui panique en cas de débordement lorsque les autres s'enroulent. Si c'est A, vous avez ajouté uint%N avec une certaine cérémonie.

L'autre option est de ne laisser aucun A, B ou C, mais c'est ce que nous avons aujourd'hui avec const/iota donc il n'y a pas de gains.

Toutes les raisons pour lesquelles vous dites que int%N ne sera pas intégré au langage semblent s'appliquer également aux énumérations qui sont en quelque sorte des entiers. (Bien que je ne serais en aucun cas en colère si quelque chose comme eux était inclus).

Enlever l'int-iness supprime cette énigme. Cela nécessite la génération de code pour les cas où vous souhaitez rajouter une partie de cette intégrité, mais cela vous donne également le choix de ne pas le faire, ce qui vous permet de contrôler la quantité d'intégrité à introduire et de quel type : vous pouvez ajouter no " next", une méthode next cyclique ou une méthode next qui renvoie une erreur si vous tombez du bord. (Vous ne vous retrouvez pas non plus avec des trucs comme Monday*Sunday - Thursday étant légal). La rigidité supplémentaire en fait un matériau de construction plus malléable. Une union discriminée modélise bien la variété non-int-y : pick { A, B, C struct{} } , entre autres choses.

Les principaux avantages d'avoir de telles informations dans la langue sont que

  1. Les valeurs illégales sont illégales.
  2. l'information est disponible pour refléter et aller / types permettant aux programmes d'agir dessus sans avoir besoin de faire des hypothèses ou des annotations (qui ne sont pas disponibles pour refléter, actuellement).

Les principaux avantages d'avoir des informations comme celle-ci dans la langue sont que : Les valeurs illégales sont illégales.

Je pense qu'il est important de souligner que tout le monde ne voit pas cela comme un avantage. Certainement pas. Cela facilite souvent la consommation de valeurs, cela rend souvent la production plus difficile. Ce que vous pesez plus lourd semble, jusqu'à présent, dépendre de vos préférences personnelles. Ainsi, il en va de même pour la question de savoir s'il s'agit d'un avantage net global.

Je ne vois pas non plus l'intérêt d'interdire les valeurs illégales. Si vous avez déjà les moyens de vérifier vous-même la validité (comme dans ma proposition ci-dessus), quel avantage cette limitation apporte-t-elle ? Pour moi, ça ne fait que compliquer les choses. Dans mes applications, les énumérations pour la majorité des cas pouvaient contenir des valeurs non valides/inconnues et vous deviez contourner cela en fonction de l'application - jeter complètement, rétrograder à une valeur par défaut ou enregistrer tel quel.

J'imagine que des énumérations strictes qui interdisent les valeurs non valides pourraient être utiles dans des cas très limités où votre application est isolée du monde extérieur et n'a aucun moyen de recevoir une entrée non valide. Comme les énumérations internes que vous seul pouvez voir et utiliser.

const avec iota n'est pas sûr au moment de la compilation, la vérification serait retardée à l'exécution et la vérification sécurisée n'est pas au niveau du type. Je pense donc que iota ne peut pas remplacer littéralement enum, je préfère enum car il est plus puissant.

Les valeurs illégales sont illégales.
Je pense qu'il est important de souligner que tout le monde ne voit pas cela comme un avantage.

Je ne comprends pas cette logique. Les types sont des ensembles de valeurs. Vous ne pouvez pas affecter un type à une variable dont la valeur n'est pas dans ce type. Est-ce que j'ai mal compris quelque chose ?

PS: Je suis d'accord que les énumérations sont un cas particulier de types de somme et que ce problème devrait avoir la priorité sur celui-ci.

Permettez-moi de reformuler/d'être plus précis : tout le monde ne considère pas la fermeture des énumérations comme un avantage.

Si vous voulez être strict de cette façon, alors a) "Les valeurs illégales sont illégales" est une tautologie et b) ne peut donc pas être compté comme un avantage. Avec les énumérations basées sur const, dans votre interprétation, les valeurs illégales sont également illégales. Le type autorise simplement beaucoup plus de valeurs.

Si les énumérations sont des entiers et que tout int est légal (du point de vue du système de type), le seul gain est que les valeurs nommées du type sont reflétées.

C'est essentiellement const/iota mais vous n'avez pas besoin d'exécuter stringer car le package fmt peut obtenir les noms en utilisant la réflexion. (Vous devriez toujours exécuter stringer si vous vouliez que les chaînes soient différentes des noms dans la source).

La stringification @jimmyfrasche n'est qu'un joli bonus. La principale caractéristique pour moi, comme vous pouvez le lire dans ma proposition ci-dessus, est la possibilité de vérifier si la valeur donnée est une valeur valide du type enum donné au moment de l'exécution.

Par exemple, étant donné quelque chose comme ça

type Foo enum {
    Val1 = 1
    Val2 = 2
}

Et la méthode de réflexion comme

func IsValidEnum(v {}interface) bool

Nous pourrions faire quelque chose comme ça

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

Pour un exemple concret, vous pouvez regarder les énumérations en C # qui, à mon avis, ont parfaitement capturé ce terrain d'entente au lieu de suivre aveuglément ce que Java a fait. Pour vérifier la validité en C#, vous utilisez Enum.IsDefined static method .

@crecker La seule différence entre cela et const/iota est le
informations stockées dans reflect. Ce n'est pas beaucoup de gain pour un tout
nouveau type de type.

Idée un peu folle :

Stockez les noms et les valeurs de tous les const déclarés dans le même package
comme leur type défini d'une manière que reflect peut atteindre. Ce serait
bizarre de distinguer cette classe étroite d'utilisation const, cependant.

La principale caractéristique pour moi, comme vous pouvez le lire dans ma proposition ci-dessus

IMO, cela illustre l'une des principales choses qui traînent cette discussion : un manque de clarté sur ce que sont les "caractéristiques principales". Tout le monde semble avoir des idées légèrement différentes à ce sujet.
Personnellement, j'aime toujours le format des rapports d'expérience pour découvrir cet ensemble. Il y en a même un dans la liste (même si, personnellement, je ferais quand même remarquer que la section "What Went Wrong" ne mentionne que ce qui pourrait mal tourner, pas ce qui s'est réellement passé ). Peut-être qu'en ajouter quelques-uns, illustrant où le manque de vérification de type conduit à des pannes/bogues ou, par exemple, à l'échec de refactorisations à grande échelle, serait utile.

@jimmyfrasche mais cela résout un gros problème dans de nombreuses applications - la validation des données d'entrée. Sans aucune aide du système de type, vous devez le faire à la main et ce n'est pas quelque chose que vous pouvez faire en quelques lignes de code. Avoir une forme de validation assistée par type résoudrait cela. L'ajout de stringification en plus de cela simplifierait la journalisation car vous auriez des noms correctement formatés et non les valeurs de type sous-jacentes.

D'un autre côté, rendre les énumérations strictes limiterait considérablement les cas d'utilisation possibles. Maintenant, vous ne pouvez pas les utiliser facilement dans des protocoles, par exemple. Pour conserver même les valeurs non valides, vous devrez supprimer les énumérations et utiliser des types de valeurs simples, éventuellement en les convertissant ultérieurement en énumérations si nécessaire. Dans certains cas, vous pouvez supprimer la valeur non valide et générer une erreur. Dans d'autres, vous pouvez revenir à une valeur par défaut. Dans tous les cas, vous vous battez avec les restrictions de votre système de type au lieu de vous aider à éviter les erreurs.

Regardez simplement ce que protobuf pour Java doit générer afin de contourner les énumérations Java.

@Merovius concernant la validation, je pense que j'ai déjà couvert cela plusieurs fois. Je ne sais pas quoi de plus pourrait être ajouté à part - sans validation, vous devez écrire d'énormes quantités de code à peu près copier-coller pour valider votre entrée. Le problème est évident, ainsi que la façon dont la solution proposée pourrait aider à cela. Je ne travaille pas sur une application à grande échelle que tout le monde connaît, mais des erreurs dans ce code de validation m'ont mordu assez de fois dans plusieurs langues avec le même concept d'énumérations que je veux voir quelque chose fait à ce sujet.

D'un autre côté, je ne vois (excuses si j'ai raté quelque chose) aucun argument en faveur de l'implémentation d'énumérations qui n'autorisent pas les valeurs invalides. C'est agréable et soigné en théorie, mais je ne le vois tout simplement pas m'aider dans les applications réelles.

Il n'y a pas beaucoup de fonctionnalités que les gens attendent des énumérations. Stringification, validation, strict/lâche en termes de valeurs non valides, énumération - c'est à peu près tout d'après ce que je peux voir. Tout le monde (y compris moi bien sûr) les mélange simplement à ce stade. strict/lâche semble être le principal point de discorde en raison de leur nature conflictuelle. Je ne pense pas que tout le monde sera d'accord avec l'un ou l'autre. Peut-être que la solution pourrait être d'incorporer les deux d'une manière ou d'une autre et de laisser le programmeur choisir, mais je ne connais aucun langage qui ait cela pour voir comment cela pourrait fonctionner dans le monde réel.

@crecker ma suggestion de stocker les consts dans les données d'exportation ci-dessus
les circonstances permettraient le genre de choses que vous demandez
sans l'introduction d'un nouveau type de type.

Je ne suis pas sûr que ce soit la manière idiomatique, et je suis aussi assez nouveau dans la langue, mais ce qui suit fonctionne et est concis

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

Avantages :

Inconvénients :

A-t-on vraiment besoin d'énumérations ?

Qu'est-ce qui empêche quelqu'un de faire quelque chose comme ça :

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

Le consommateur d'une telle variable peut ou non comprendre cela avec une vérification appropriée des valeurs attendues (en supposant qu'il n'y a pas de mauvaise chute à travers les hypothèses dans les instructions de commutation, comme tout ce qui n'est pas samedi ou dimanche est un jour de semaine, par exemple), mais ce ne serait pas jusqu'à l'exécution. Je pense que l'on préférerait que ce type d'erreur soit détecté au moment de la compilation, pas au moment de l'exécution.

@bpkroth
En ayant Day dans son propre package et en n'exposant que les champs et méthodes sélectionnés, je ne peux pas créer de nouvelles valeurs de type Day en dehors de package day
De plus, de cette façon, je ne peux pas transmettre de structures anonymes à getTask

./jour/jour.aller

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

Je n'ai jamais vu de toute ma vie un autre langage qui insiste pour ne pas ajouter les fonctionnalités les plus simples et les plus utiles comme les énumérations, les opérateurs ternaires, la compilation avec des variables inutilisées, les types somme, les génériques, les paramètres par défaut, etc...

Golang est-il une expérience sociale pour voir à quel point les développeurs peuvent être stupides ?

@gh67uyyghj Quelqu'un a marqué votre commentaire comme hors sujet ! et je suppose que quelqu'un fera de même pour ma réponse. mais je suppose que la réponse à votre question est OUI. Dans GoLang, être sans fonctionnalité signifie être fonctionnel, donc tout ce que GoLang n'a pas est en fait une fonctionnalité que GoLang a et que les autres langages de programmation n'ont pas !!

@L-oris C'est une façon très intéressante d'implémenter des énumérations avec des types. Mais cela semble gênant, et avoir un mot-clé enum (qui complique nécessairement un peu plus le langage) faciliterait :

  • écrivez
  • lire
  • raisonner à propos

Dans votre exemple (ce qui est génial car cela fonctionne aujourd'hui), avoir des énumérations (sous une forme ou une autre) implique la nécessité de :

  • Créer un type de structure
  • Créer une méthode
  • Créer des variables (pas même des constantes, bien qu'un utilisateur de la bibliothèque ne puisse pas modifier ces valeurs)

Cela prend plus de temps (mais pas beaucoup plus longtemps) à lire, écrire et raisonner (discernez qu'il représente et doit être utilisé comme énumération).

Par conséquent, je pense que la proposition de syntaxe frappe la bonne note en termes de simplicité et de valeur ajoutée au langage.

Merci @andradei
Oui, c'est une solution de contournement, mais je pense que le but du langage est de le garder petit et simple
Nous pourrions également dire que nous manquons des cours, mais passons simplement à Java :)

Je préfère me concentrer sur les propositions Go 2, une meilleure gestion des erreurs, par exemple. me fournirait bien plus de valeur que ces énumérations

Revenons à vos points :

  • ce n'est pas tellement passe-partout; au pire, on peut avoir des générateurs (mais, est-ce vraiment autant de code ?)
  • quel degré de « simplicité » obtenons-nous en ajoutant un nouveau mot-clé, et tout l'ensemble spécifique de comportements qu'il aura probablement ?
  • être un peu créatif avec les méthodes peut également ajouter des fonctionnalités intéressantes à ces énumérations
  • pour la lisibilité, il s'agit plutôt de s'y habituer ; peut-être ajouter un commentaire dessus, ou préfixer vos variables
package day

// Day Enum
type Day struct {
    value string
}

@L-oris je vois. Je suis également enthousiasmé par les propositions de Go 2. Je dirais que les génériques augmenteront la complexité du langage plus que les énumérations. Mais pour s'en tenir à vos points:

  • Ce n'est pas si passe-partout en effet
  • Nous devrions vérifier à quel point le concept d'énumération est connu pour être certain, je dirais que la plupart des gens savent ce que c'est (mais je ne peux pas le prouver). La complexité de la langue aurait un bon "prix" à payer pour ses avantages.
  • C'est vrai, ne pas avoir d'énumérations sont des problèmes qui ne me sont apparus que lors de la vérification du code generetad protobuf et lors de la tentative de création d'un modèle de base de données qui imite une énumération, par exemple.
  • C'est également vrai.

J'ai beaucoup réfléchi à cette proposition, et je peux voir la grande valeur de la simplicité sur la productivité, et pourquoi vous penchez pour la conserver à moins qu'un changement ne soit clairement nécessaire. Les énumérations pourraient également changer la langue si radicalement qu'elle n'est plus Go, et évaluer les avantages/inconvénients de cela semble prendre beaucoup de temps. J'ai donc pensé que des solutions simples comme la vôtre, où le code est toujours facile à lire, sont une bonne solution au moins pour l'instant.

Les gars, je veux vraiment cette fonctionnalité pour l'avenir !. Les pointeurs et la façon de définir les "énumérations" dans _de nos jours_ ne s'entendent pas très bien. Par exemple : https://play.golang.org/p/A7rjgAMjfCx

Ma proposition pour enum suit. Nous devrions considérer cela comme un nouveau type. Par exemple, je voudrais utiliser le type enum avec une structure arbitraire et l'implémentation suivante :

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

Il est compréhensible comment marshall cette structure et comment travailler et comment changer de chaîne et ainsi de suite.
Et bien sûr, si je pouvais remplacer la fonction "Suivant", ce serait génial.

Pour cela, Go devrait d'abord prendre en charge les structures profondes immuables. Sans types immuables, je peux imaginer que vous pourriez le faire avec des énumérations pour avoir la même chose :

type Status enum {
  Started
  Stopped
}

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

je pense que ça devrait paraitre plus simple

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

Proposition pour Go2

Logiquement, les énumérations sont censées fournir une interface de type.
J'ai déclaré plus tôt des énumérations censées être séparées.
Il s'agit de constantes explicitement nommées liées à un espace de noms spécifique.

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

Les énumérations sont des extensions de type, des "conteneurs de constantes".

Pour les amoureux des types

Alternatives de syntaxe pour ceux qui veulent le voir comme type

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

Mais nous pouvons également éviter ces déclarations explicites de haut niveau

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

L'exemple de validation reste le même.

mais au cas où

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
}

Qu'en est-il de Status1.Started == Status2.Started ?
sur le marshaling ?

Si je change de poste ?

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

Je suis d'accord avec @Goodwine sur les types immuables.

Le marshaling est une question intéressante.
Tout dépend de la manière dont nous allons traiter la valeur sous-jacente. Si nous allons utiliser des valeurs réelles, donc Status1.Started serait égal à Status2.Started .
Si nous optons pour une interprétation symbolique, celles-ci seraient considérées comme des valeurs différentes.

L'insertion de quelque chose entraînera un changement de valeurs (exactement de la même manière que pour iota ).
Pour éviter cela, le développeur doit spécifier des valeurs parallèlement aux déclarations.

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

C'est une évidence.
Si nous voulons éviter de tels problèmes, nous devons fournir une sortie de compilateur prévisible basée sur l'interprétation lexicale des valeurs enum. Je suppose que la manière la plus simple - construire une table de hachage ou s'en tenir à des noms symboliques (chaînes) à moins que la conversion de type personnalisée ne soit définie.

J'aime la façon dont Rust est implémenté Enums.

Par défaut sans type spécifié

enum IpAddr {
    V4,
    V6,
}

Type personnalisé

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

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

Types complexes

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

Bien sûr, même avoir des énumérations simples comme en C # qui sont stockées sous forme de types intégraux serait formidable.

Ce qui précède va au-delà enum s, ce sont des _unions discriminées_, qui sont en effet plus puissantes, en particulier avec _pattern matching_, qui pourrait être une extension mineure de switch , quelque chose comme :

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

Je n'ai pas besoin de vérifications du temps de compilation des énumérations, car cela pourrait être dangereux comme mentionné

Ce dont j'avais besoin plusieurs fois aurait été d'itérer sur toutes les constantes d'un type donné:

  • soit pour la validation (si nous sommes très sûrs de ne vouloir accepter que cela ou simplement ignorer les options inconnues)

    • ou pour une liste de constantes possibles (pensez aux listes déroulantes).

On pourrait faire la validation avec iota et en précisant la fin de la liste. Cependant, utiliser iota pour autre chose que juste à l'intérieur du code serait assez dangereux car des choses se casseraient en insérant une constante à la mauvaise ligne (je sais que nous devons savoir où nous mettons les choses dans la programmation, mais un bogue comme celui-ci est beaucoup plus difficile à trouver que d'autres choses). De plus, nous n'avons aucune description de ce que représente réellement la constante lorsqu'il s'agit d'un nombre. Cela nous amène au point suivant :

Un bon plus serait de spécifier des noms de chaîne pour cela.

Qu'est-ce qui empêche quelqu'un de faire quelque chose comme ça :

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

Le consommateur d'une telle variable peut ou non comprendre cela avec une vérification appropriée des valeurs attendues (en supposant qu'il n'y a pas de mauvaise chute à travers les hypothèses dans les instructions de commutation, comme tout ce qui n'est pas samedi ou dimanche est un jour de semaine, par exemple), mais ce ne serait pas jusqu'à l'exécution. Je pense que l'on préférerait que ce type d'erreur soit détecté au moment de la compilation, pas au moment de l'exécution.

@L-oris Alors qu'en est-il de ça:

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
}

Ce que nous voulons, ce n'est PAS RUNTIME SILENCE & Weird BUG causé par le [retour "rien à faire"] mais un RAPPORT D'ERREUR au moment de la compilation / du codage !
COMPRENDRE?

  1. enum est en effet un nouveau type, ce que fait type State string , il n'est pas nécessaire d'introduire un nouveau mot-clé. Go ne consiste pas à économiser de l'espace dans votre code source, il s'agit de lisibilité, de clarté d'objectif.

  2. Le manque de sécurité de type, confondre les nouveaux types basés sur string - ou int pour les chaînes/entiers réels est le principal obstacle. Toutes les clauses enum sont déclarées comme const , ce qui crée un ensemble de valeurs connues que le compilateur peut vérifier.

  3. L'interface Stringer est l'idiome pour représenter n'importe quel type sous forme de texte lisible par l'homme. Sans personnalisation, type ContextKey string enums c'est la valeur de chaîne, et pour iota -enums générés c'est l'entier, un peu comme les codes XHR ReadyState (0 - non envoyé, 4 - fait) en JavaScript.

    Le problème réside plutôt dans la faillibilité de l'implémentation personnalisée de func (k ContextKey) String() string , qui est généralement effectuée à l'aide d'un commutateur qui doit contenir toutes les constantes de clause enum connues.

  4. Dans un langage comme Swift, il existe une notion de _commutateur exhaustif_. C'est une bonne approche à la fois pour la vérification de type par rapport à un ensemble de const s et pour la construction d'une manière idiomatique d'invoquer cette vérification. La fonction String() , étant une nécessité courante, est un excellent exemple de mise en œuvre.

Proposition

package main

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

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

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

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

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

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

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

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

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

PS Les valeurs associées dans les énumérations Swift sont l'un de mes gadgets préférés. Au Go, il n'y a pas de place pour eux. Si vous voulez avoir une valeur à côté de vos données d'énumération, utilisez un struct fortement typé enveloppant les deux.

Il y a quelques mois, j'ai écrit une preuve de concept pour un linter qui vérifie que les types énumérés sont correctement gérés. https://github.com/loov/enumcheck

Actuellement, il utilise des commentaires pour marquer les éléments comme des énumérations :

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

Je suis resté coincé à comprendre comment gérer toutes les conversions implicites, mais cela fonctionne correctement pour les cas de base.

Notez qu'actuellement, il s'agit toujours d'un travail en cours, donc les choses peuvent changer. par exemple, au lieu de commentaires, il pourrait utiliser un paquet stub pour annoter les types, mais les commentaires sont assez bons pour le moment.

L'implémentation actuelle d'énumérations dans Go1 est l'implémentation d'énumération la plus étrange et la plus inévidente dans toutes les langues que je connaisse. Même C les implémente mieux. Le truc iota ressemble à un hack. Et que diable signifie iota de toute façon ? Comment suis-je censé mémoriser ce mot-clé ? Go est censé être facile à apprendre. Mais c'est juste qiurky.

@pofl :
Bien que je convienne que les énumérations Go sont assez gênantes, iota n'est en fait qu'un mot anglais normal :

iota
_nom_

  1. une très petite quantité; iota; brin.
  2. la neuvième lettre de l'alphabet grec (I, ι).
  3. le son vocalique représenté par cette lettre.

Vraisemblablement, ils optaient pour la première définition en termes d'utilisation dans la langue.

Sur une note en réponse à un commentaire plus ancien ici:
Bien que j'aimerais vraiment les unions discriminées dans Go, j'ai l'impression qu'elles devraient être séparées des énumérations réelles. Avec la façon dont les génériques se déroulent actuellement, vous pouvez en fait obtenir quelque chose de très similaire aux unions discriminées via des listes de types dans les interfaces. Voir #41716.

L'utilisation de iota dans Go est vaguement basée sur son utilisation dans APL. Citant https://en.wikipedia.org/wiki/Iota :

Dans certains langages de programmation (par exemple, A+, APL, C++[6], Go[7]), iota (soit le symbole minuscule ⍳ soit l'identifiant iota) est utilisé pour représenter et générer un tableau d'entiers consécutifs. Par exemple, dans APL ⍳4 donne 1 2 3 4.

Cette page vous a été utile?
0 / 5 - 0 notes