Quelle est la manière idiomatique de représenter les énumérations dans Go?

523

J'essaie de représenter un chromosome simplifié, qui se compose de N bases, dont chacune ne peut être qu'une {A, C, T, G}.

J'aimerais formaliser les contraintes avec une énumération, mais je me demande quelle est la manière la plus idiomatique d'émuler une énumération dans Go.

carbocation
la source
4
Dans les packages go standard, ils sont représentés sous forme de constantes. Voir golang.org/pkg/os/#pkg-constants
Denys Séguret
Copie
icza
7
@icza Cette question a été posée 3 ans auparavant. Cela ne peut pas être un double de celui-ci, en supposant que la flèche du temps est en état de marche.
carbocation
Consultez le guide ultime pour énumérer .
Inanc Gumus

Réponses:

659

Citant des spécifications linguistiques: Iota

Dans une déclaration constante, l'identifiant prédéclaré iota représente des constantes entières non typées successives. Il est remis à 0 chaque fois que le mot réservé const apparaît dans la source et s'incrémente après chaque ConstSpec. Il peut être utilisé pour construire un ensemble de constantes connexes:

const (  // iota is reset to 0
        c0 = iota  // c0 == 0
        c1 = iota  // c1 == 1
        c2 = iota  // c2 == 2
)

const (
        a = 1 << iota  // a == 1 (iota has been reset)
        b = 1 << iota  // b == 2
        c = 1 << iota  // c == 4
)

const (
        u         = iota * 42  // u == 0     (untyped integer constant)
        v float64 = iota * 42  // v == 42.0  (float64 constant)
        w         = iota * 42  // w == 84    (untyped integer constant)
)

const x = iota  // x == 0 (iota has been reset)
const y = iota  // y == 0 (iota has been reset)

Dans une ExpressionList, la valeur de chaque iota est la même car elle n'est incrémentée qu'après chaque ConstSpec:

const (
        bit0, mask0 = 1 << iota, 1<<iota - 1  // bit0 == 1, mask0 == 0
        bit1, mask1                           // bit1 == 2, mask1 == 1
        _, _                                  // skips iota == 2
        bit3, mask3                           // bit3 == 8, mask3 == 7
)

Ce dernier exemple exploite la répétition implicite de la dernière liste d'expressions non vide.


Donc, votre code pourrait être comme

const (
        A = iota
        C
        T
        G
)

ou

type Base int

const (
        A Base = iota
        C
        T
        G
)

si vous voulez que les bases soient un type distinct de int.

zzzz
la source
16
grands exemples (je ne me souviens pas du comportement exact de l'iota - quand il est incrémenté - de la spécification). Personnellement, j'aime donner un type à une énumération, donc elle peut être vérifiée par type lorsqu'elle est utilisée comme argument, champ, etc.
mna
16
@Jnml très intéressant. Mais je suis un peu déçu que la vérification de type statique semble lâche, par exemple rien ne m'empêche d'utiliser la Base n ° 42 qui n'a jamais existé: play.golang.org/p/oH7eiXBxhR
Deleplace
4
Go n'a pas de concept de types de sous-plages numériques, comme par exemple celui de Pascal, il Ord(Base)n'est donc pas limité à 0..3mais a les mêmes limites que son type numérique sous-jacent. C'est un choix de conception de langage, compromis entre sécurité et performance. Envisagez à chaque fois des vérifications liées au temps d'exécution "en toute sécurité" lorsque vous touchez une Basevaleur saisie. Ou comment définir le comportement de «débordement» de la Basevaleur pour l'arithmétique et pour ++et --? Etc.
zzzz
7
Pour compléter en jnml, même sémantiquement, rien dans le langage ne dit que les consts définis comme Base représentent la gamme entière de Base valide, il dit juste que ces consts particuliers sont de type Base. Plus de constantes pourraient également être définies ailleurs comme Base, et ce n'est même pas mutuellement exclusif (par exemple, const Z Base = 0 pourrait être défini et serait valide).
mna
10
Vous pouvez utiliser iota + 1pour ne pas commencer à 0.
Marçal Juan
87

En vous référant à la réponse de jnml, vous pouvez empêcher de nouvelles instances de type Base en n'exportant pas du tout le type Base (c'est-à-dire l'écrire en minuscules). Si nécessaire, vous pouvez créer une interface exportable qui a une méthode qui renvoie un type de base. Cette interface pourrait être utilisée dans des fonctions de l'extérieur qui traitent des bases, c'est-à-dire

package a

type base int

const (
    A base = iota
    C
    T
    G
)


type Baser interface {
    Base() base
}

// every base must fulfill the Baser interface
func(b base) Base() base {
    return b
}


func(b base) OtherMethod()  {
}

package main

import "a"

// func from the outside that handles a.base via a.Baser
// since a.base is not exported, only exported bases that are created within package a may be used, like a.A, a.C, a.T. and a.G
func HandleBasers(b a.Baser) {
    base := b.Base()
    base.OtherMethod()
}


// func from the outside that returns a.A or a.C, depending of condition
func AorC(condition bool) a.Baser {
    if condition {
       return a.A
    }
    return a.C
}

L'intérieur du package principal a.Baserest désormais comme une énumération. Ce n'est qu'à l'intérieur d'un package que vous pouvez définir de nouvelles instances.

metakeule
la source
10
Votre méthode semble parfaite pour les cas où elle basen'est utilisée que comme récepteur de méthode. Si votre apackage exposait une fonction prenant un paramètre de type base, cela deviendrait dangereux. En effet, l'utilisateur pourrait simplement l'appeler avec la valeur littérale 42, que la fonction accepterait comme basepuisqu'elle peut être castée en entier. Pour éviter cela, faire baseun struct: type base struct{value:int}. Problème: vous ne pouvez plus déclarer les bases comme constantes, uniquement les variables de module. Mais 42 ne seront jamais convertis en un basede ce type.
Niriel
6
@metakeule J'essaie de comprendre votre exemple mais votre choix de noms de variables a rendu extrêmement difficile.
anon58192932
1
C'est l'un de mes bogues dans les exemples. FGS, je me rends compte que c'est tentant, mais ne nommez pas la variable de la même manière que le type!
Graham Nicholls
27

Vous pouvez le faire ainsi:

type MessageType int32

const (
    TEXT   MessageType = 0
    BINARY MessageType = 1
)

Avec ce compilateur de code devrait vérifier le type d'énumération

Azat
la source
5
Les constantes sont généralement écrites en chameau normal, pas toutes en majuscules. La lettre majuscule initiale signifie que la variable est exportée, ce qui peut ou non être ce que vous voulez.
2017 à 42h16
1
J'ai remarqué dans le code source de Go qu'il y a un mélange où parfois les constantes sont toutes en majuscules et parfois elles sont en camel. Avez-vous une référence à une spécification?
Jeremy Gailor
@JeremyGailor Je pense que 425nesp note simplement que la préférence normale est que les développeurs les utilisent comme constantes non exportées, alors utilisez camelcase. Si le développeur détermine qu'il doit être exporté, n'hésitez pas à utiliser toutes les majuscules ou les majuscules car il n'y a pas de préférence établie. Voir les recommandations de révision du code de Golang et la section Go efficace sur les constantes
waynethec
Il y a une préférence. Tout comme les variables, fonctions, types et autres, les noms constants doivent être mixedCaps ou MixedCaps, pas ALLCAPS. Source: Commentaires sur la révision du code Go .
Rodolfo Carvalho
Notez que, par exemple, les fonctions qui attendent un MessageType accepteront avec plaisir des constantes numériques non typées, par exemple 7. En outre, vous pouvez convertir n'importe quel int32 en MessageType. Si vous en êtes conscient, je pense que c'est la voie la plus idiomatique.
Kosta Il y a
23

Il est vrai que les exemples ci-dessus d'utilisation de constet iotasont les façons les plus idiomatiques de représenter des énumérations primitives dans Go. Mais que se passe-t-il si vous cherchez un moyen de créer une énumération plus complète similaire au type que vous verriez dans un autre langage comme Java ou Python?

Un moyen très simple de créer un objet qui commence à ressembler à une énumération de chaînes en Python serait:

package main

import (
    "fmt"
)

var Colors = newColorRegistry()

func newColorRegistry() *colorRegistry {
    return &colorRegistry{
        Red:   "red",
        Green: "green",
        Blue:  "blue",
    }
}

type colorRegistry struct {
    Red   string
    Green string
    Blue  string
}

func main() {
    fmt.Println(Colors.Red)
}

Supposons que vous vouliez également des méthodes utilitaires, comme Colors.List()et Colors.Parse("red"). Et vos couleurs étaient plus complexes et devaient être une structure. Ensuite, vous pourriez faire quelque chose comme ceci:

package main

import (
    "errors"
    "fmt"
)

var Colors = newColorRegistry()

type Color struct {
    StringRepresentation string
    Hex                  string
}

func (c *Color) String() string {
    return c.StringRepresentation
}

func newColorRegistry() *colorRegistry {

    red := &Color{"red", "F00"}
    green := &Color{"green", "0F0"}
    blue := &Color{"blue", "00F"}

    return &colorRegistry{
        Red:    red,
        Green:  green,
        Blue:   blue,
        colors: []*Color{red, green, blue},
    }
}

type colorRegistry struct {
    Red   *Color
    Green *Color
    Blue  *Color

    colors []*Color
}

func (c *colorRegistry) List() []*Color {
    return c.colors
}

func (c *colorRegistry) Parse(s string) (*Color, error) {
    for _, color := range c.List() {
        if color.String() == s {
            return color, nil
        }
    }
    return nil, errors.New("couldn't find it")
}

func main() {
    fmt.Printf("%s\n", Colors.List())
}

À ce stade, bien sûr, cela fonctionne, mais vous pourriez ne pas aimer la façon dont vous devez définir les couleurs de manière répétitive. Si à ce stade, vous souhaitez éliminer cela, vous pouvez utiliser des balises sur votre structure et faire quelques réflexions pour le configurer, mais j'espère que cela suffit pour couvrir la plupart des gens.

Becca Petrin
la source
19

Depuis Go 1.4, l' go generateoutil a été introduit avec la stringercommande qui rend votre énumération facilement débogable et imprimable.

Moshe Revah
la source
Savez-vous que c'est une solution oposite. Je veux dire chaîne -> MyType. Puisqu'une solution à sens unique est loin d'être idéale. Voici l' essentiel de sb qui fait ce que je veux - mais écrire à la main est facile de faire des erreurs.
SR
11

Je suis sûr que nous avons ici beaucoup de bonnes réponses. Mais, j'ai juste pensé à ajouter la façon dont j'ai utilisé les types énumérés

package main

import "fmt"

type Enum interface {
    name() string
    ordinal() int
    values() *[]string
}

type GenderType uint

const (
    MALE = iota
    FEMALE
)

var genderTypeStrings = []string{
    "MALE",
    "FEMALE",
}

func (gt GenderType) name() string {
    return genderTypeStrings[gt]
}

func (gt GenderType) ordinal() int {
    return int(gt)
}

func (gt GenderType) values() *[]string {
    return &genderTypeStrings
}

func main() {
    var ds GenderType = MALE
    fmt.Printf("The Gender is %s\n", ds.name())
}

C'est de loin l'un des moyens idiomatiques que nous pourrions créer des types énumérés et utiliser dans Go.

Éditer:

Ajout d'une autre façon d'utiliser les constantes pour énumérer

package main

import (
    "fmt"
)

const (
    // UNSPECIFIED logs nothing
    UNSPECIFIED Level = iota // 0 :
    // TRACE logs everything
    TRACE // 1
    // INFO logs Info, Warnings and Errors
    INFO // 2
    // WARNING logs Warning and Errors
    WARNING // 3
    // ERROR just logs Errors
    ERROR // 4
)

// Level holds the log level.
type Level int

func SetLogLevel(level Level) {
    switch level {
    case TRACE:
        fmt.Println("trace")
        return

    case INFO:
        fmt.Println("info")
        return

    case WARNING:
        fmt.Println("warning")
        return
    case ERROR:
        fmt.Println("error")
        return

    default:
        fmt.Println("default")
        return

    }
}

func main() {

    SetLogLevel(INFO)

}
wandermonk
la source
2
Vous pouvez déclarer des constantes avec des valeurs de chaîne. OMI, il est plus facile de le faire si vous avez l'intention de les afficher et n'avez pas réellement besoin de la valeur numérique.
cbednarski
4

Voici un exemple qui s'avérera utile en cas de nombreuses énumérations. Il utilise des structures à Golang et s'appuie sur des principes orientés objet pour les lier tous ensemble dans un petit paquet soigné. Aucun du code sous-jacent ne changera lorsqu'une nouvelle énumération est ajoutée ou supprimée. Le processus est le suivant:

  • Définissez une structure d'énumération pour enumeration items: EnumItem . Il a un type entier et chaîne.
  • Définissez le enumerationcomme une liste de enumeration items: Enum
  • Construisez des méthodes pour l'énumération. Quelques-uns ont été inclus:
    • enum.Name(index int): renvoie le nom de l'index donné.
    • enum.Index(name string): renvoie le nom de l'index donné.
    • enum.Last(): retourne l'index et le nom de la dernière énumération
  • Ajoutez vos définitions d'énumération.

Voici du code:

type EnumItem struct {
    index int
    name  string
}

type Enum struct {
    items []EnumItem
}

func (enum Enum) Name(findIndex int) string {
    for _, item := range enum.items {
        if item.index == findIndex {
            return item.name
        }
    }
    return "ID not found"
}

func (enum Enum) Index(findName string) int {
    for idx, item := range enum.items {
        if findName == item.name {
            return idx
        }
    }
    return -1
}

func (enum Enum) Last() (int, string) {
    n := len(enum.items)
    return n - 1, enum.items[n-1].name
}

var AgentTypes = Enum{[]EnumItem{{0, "StaffMember"}, {1, "Organization"}, {1, "Automated"}}}
var AccountTypes = Enum{[]EnumItem{{0, "Basic"}, {1, "Advanced"}}}
var FlagTypes = Enum{[]EnumItem{{0, "Custom"}, {1, "System"}}}
Aaron
la source