Jaki jest idiomatyczny sposób reprezentowania wyliczeń w Go?

522

Próbuję przedstawić uproszczony chromosom, który składa się z N zasad, z których każda może być tylko jedną z nich {A, C, T, G}.

Chciałbym sformalizować ograniczenia za pomocą wyliczenia, ale zastanawiam się, jaki najbardziej idiomatyczny sposób naśladowania wyliczenia jest w Go.

karbokokacja
źródło
4
W go standardowe pakiety są reprezentowane jako stałe. Zobacz golang.org/pkg/os/#pkg-constants
Denys Séguret,
2
powiązane: stackoverflow.com/questions/14236263/…
lbonn 20.01.2013
7
@icza Pytanie zostało zadane 3 lata wcześniej. To nie może być duplikat tego, zakładając, że strzała czasu działa.
carbocation
Sprawdź ostateczny przewodnik do wyliczania .
Inanc Gumus,

Odpowiedzi:

658

Cytowanie ze specyfikacji językowej: Iota

W ramach stałej deklaracji z góry zadeklarowany identyfikator iota reprezentuje kolejne stałe bez liczb całkowitych. Jest zerowane za każdym razem, gdy zarezerwowane słowo const pojawia się w źródle i zwiększa się po każdym ConstSpec. Można go użyć do skonstruowania zestawu powiązanych stałych:

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)

W obrębie ExpressionList wartość każdego jota jest taka sama, ponieważ jest zwiększana tylko po każdym 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
)

Ten ostatni przykład wykorzystuje niejawne powtórzenie ostatniej niepustej listy wyrażeń.


Twój kod może być podobny

const (
        A = iota
        C
        T
        G
)

lub

type Base int

const (
        A Base = iota
        C
        T
        G
)

jeśli chcesz, aby zasady były odrębnym typem od int.

zzzz
źródło
16
świetne przykłady (nie przypominałem sobie dokładnego zachowania Iota - gdy jest zwiększane - ze specyfikacji). Osobiście lubię nadawać typ wyliczeniu, aby można go było sprawdzić jako argument, pole itp.
2013
16
Bardzo ciekawe @jnml. Ale jestem trochę rozczarowany, że statyczne sprawdzanie typu wydaje się luźne, na przykład nic nie powstrzymuje mnie przed użyciem bazy nr 42, która nigdy nie istniała: play.golang.org/p/oH7eiXBxhR
Deleplace
4
Go nie ma pojęcia liczbowych typów podzakresów, takich jak np. Pascala, więc Ord(Base)nie ogranicza się do, 0..3ale ma takie same ograniczenia jak podstawowy typ liczbowy. To wybór języka, kompromis między bezpieczeństwem a wydajnością. Rozważ „bezpieczne” kontrole czasowe za każdym razem, gdy dotykaszBase wpisanej wartości. Lub jak zdefiniować zachowanie „przepełnienia” Basewartości dla arytmetyki oraz dla ++i --? Itd.
zzzz,
7
Aby uzupełnić jnml, nawet semantycznie, nic w tym języku nie mówi, że consts zdefiniowane jako Base reprezentują cały zakres prawidłowej Base, to po prostu mówi, że te konkretne stałe są typu Base. Więcej stałych można również zdefiniować gdzie indziej jako Podstawa, i nie wyklucza się to nawet wzajemnie (np. Const Z Base = 0 może być zdefiniowane i byłoby ważne).
2013
10
Możesz użyć, iota + 1aby nie zaczynać od 0
Marçal Juan
87

Odnosząc się do odpowiedzi jnml, możesz zapobiec nowym wystąpieniom typu podstawowego, nie eksportując w ogóle typu podstawowego (tj. Zapisuj go małymi literami). W razie potrzeby możesz utworzyć interfejs do eksportu, który ma metodę zwracającą typ podstawowy. Interfejs ten może być wykorzystywany w funkcjach z zewnątrz, które dotyczą baz, tj

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
}

Wewnątrz głównego pakietu a.Baserjest teraz faktycznie jak wyliczanka. Tylko w pakiecie możesz definiować nowe instancje.

metakeule
źródło
10
Twoja metoda wydaje się idealna w przypadkach, gdy basejest używana tylko jako odbiornik metody. Gdyby twój apakiet ujawnił funkcję przyjmującą parametr typu base, stałby się niebezpieczny. Rzeczywiście, użytkownik może po prostu wywołać go z wartością dosłowną 42, którą funkcja zaakceptowałaby, baseponieważ można go rzutować na liczbę całkowitą. Aby temu zapobiec, należy : . Problem: nie można już deklarować baz jako stałych, tylko zmienne modułowe. Ale 42 nigdy nie zostanie obsadzone na tego typu. basestructtype base struct{value:int}base
Niriel
6
@metakeule Próbuję zrozumieć twój przykład, ale twój wybór w nazwach zmiennych sprawił, że jest to niezwykle trudne.
anon58192932
1
To jeden z moich błędów w przykładach. FGS, zdaję sobie sprawę, że to kuszące, ale nie nazywaj zmiennej tak jak typ!
Graham Nicholls
26

Możesz to zrobić tak:

type MessageType int32

const (
    TEXT   MessageType = 0
    BINARY MessageType = 1
)

Za pomocą tego kodu kompilator powinien sprawdzać typ wyliczenia

Azat
źródło
5
Stałe są zwykle zapisywane w zwykłej wielbłądzie, nie wszystkie wielkie litery. Początkowa wielka litera oznacza, że ​​zmienna jest eksportowana, co może być lub nie być tym, czego chcesz.
425nesp,
1
Zauważyłem, że w kodzie źródłowym Go jest mieszanka, w której czasami stałe są wielkimi literami, a czasem wielbłądami. Czy masz odniesienie do specyfikacji?
Jeremy Gailor,
@JeremyGailor Myślę, że 425nesp po prostu zauważa, że ​​normalną preferencją jest, aby programiści używali ich jako nieeksportowanych stałych, więc używaj obudowy wielbłąda . Jeśli programista zdecyduje, że należy go wyeksportować, możesz użyć wielkiej lub dużej litery, ponieważ nie ma ustalonych preferencji. Zobacz sekcję Zalecenia dotyczące przeglądu kodu Golang i efektywnego działania na temat stałych
waynethec
Istnieje preferencja. Podobnie jak zmienne, funkcje, typy i inne, stałe nazwy powinny być mixedCaps lub MixedCaps, a nie ALLCAPS. Źródło: Komentarze Go Code Review .
Rodolfo Carvalho
22

To prawda, że ​​powyższe przykłady użycia consti iotasą najbardziej idiomatycznymi sposobami reprezentowania prymitywnych wyliczeń w Go. Ale co, jeśli szukasz sposobu na stworzenie w pełni funkcjonalnego wyliczenia podobnego do typu, który można zobaczyć w innym języku, takim jak Java lub Python?

Bardzo prostym sposobem na utworzenie obiektu, który zaczyna wyglądać i zachowywać się jak wyliczenie ciągu w Pythonie, byłoby:

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

Załóżmy, że chciałeś także mieć kilka metod narzędziowych, takich jak Colors.List()i Colors.Parse("red"). A twoje kolory były bardziej złożone i musiały być strukturą. Następnie możesz zrobić coś takiego:

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

W tym momencie na pewno działa, ale może ci się nie podobać powtarzalne definiowanie kolorów. Jeśli w tym momencie chcesz to wyeliminować, możesz użyć tagów na swojej strukturze i zrobić trochę wymyślnego zastanowienia, aby to skonfigurować, ale mam nadzieję, że to wystarczy, aby pokryć większość ludzi.

Becca Petrin
źródło
19

Począwszy od wersji 1.4 go generatenarzędzie zostało wprowadzone wraz z stringerpoleceniem, które sprawia, że ​​wyliczanie jest łatwe do debugowania i drukowania.

Moshe Revah
źródło
Czy wiesz, że jest to rozwiązanie przeciwne? Mam na myśli string -> MyType. Ponieważ rozwiązanie jednokierunkowe jest dalekie od ideału. Oto lista zadań, które robią, co chcę - ale pisanie ręczne jest łatwe do popełniania błędów.
SR
11

Jestem pewien, że mamy tutaj wiele dobrych odpowiedzi. Ale właśnie pomyślałem o dodaniu sposobu, w jaki użyłem typów wyliczonych

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

Jest to zdecydowanie jeden z idiomatycznych sposobów, w jaki moglibyśmy tworzyć typy wyliczone i używać ich w Go.

Edytować:

Dodanie innego sposobu użycia stałych do wyliczenia

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
źródło
2
Można zadeklarować stałe za pomocą wartości ciągu. IMO łatwiej to zrobić, jeśli zamierzasz je wyświetlić i tak naprawdę nie potrzebujesz wartości liczbowej.
cbednarski
4

Oto przykład, który okaże się przydatny, gdy istnieje wiele wyliczeń. Korzysta ze struktur w Golang i czerpie z Object Oriented Principles, aby związać je wszystkie razem w zgrabny mały pakiet. Żaden z podstawowych kodów nie zmieni się, gdy nowe wyliczenie zostanie dodane lub usunięte. Proces jest następujący:

  • Zdefiniuj strukturę wyliczenia dla enumeration items: EnumItem . Ma liczbę całkowitą i ciąg znaków.
  • Zdefiniuj enumerationjako listę enumeration items: Enum
  • Zbuduj metody wyliczenia. Uwzględniono kilka:
    • enum.Name(index int): zwraca nazwę dla podanego indeksu.
    • enum.Index(name string): zwraca nazwę dla podanego indeksu.
    • enum.Last(): zwraca indeks i nazwę ostatniego wyliczenia
  • Dodaj definicje wyliczeń.

Oto kod:

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
źródło