Что такое идиоматический способ представления перечислений в Go?

523

Я пытаюсь представить упрощенную хромосому, которая состоит из N оснований, каждое из которых может быть только одним из {A, C, T, G}.

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

карбкатион
источник
4
В стандартных пакетах go они представлены как константы. См. Golang.org/pkg/os/#pkg-constants
Денис Сегюре
2
связанные: stackoverflow.com/questions/14236263/…
lbonn
7
@icza Этот вопрос был задан за 3 года до этого. Это не может быть дубликатом этого, предполагая, что стрелка времени в рабочем состоянии.
карбокатион

Ответы:

659

Цитирование из спецификации языка: Йота

В объявлении константы предварительно объявленный идентификатор iota представляет последовательные нетипизированные целочисленные константы. Он сбрасывается в 0 всякий раз, когда зарезервированное слово const появляется в источнике и увеличивается после каждого ConstSpec. Его можно использовать для построения набора связанных констант:

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)

В ExpressionList значение каждой йоты одинаково, поскольку оно увеличивается только после каждого 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
)

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


Таким образом, ваш код может быть как

const (
        A = iota
        C
        T
        G
)

или

type Base int

const (
        A Base = iota
        C
        T
        G
)

если вы хотите, чтобы основания были отдельным типом от int.

ZZZZ
источник
16
отличные примеры (я не вспомнил точное поведение йоты - когда оно увеличивается - из спецификации). Лично мне нравится давать тип перечислению, чтобы его можно было проверять при использовании в качестве аргумента, поля и т. Д.
mna
16
Очень интересно @jnml. Но я отчасти разочарован тем, что статическая проверка типов кажется бесполезной, например, ничто не мешает мне использовать Base № 42, которой никогда не было: play.golang.org/p/oH7eiXBxhR
Deleplace
4
Go не имеет понятия о числовых типах поддиапазонов, как, например, у Паскаля, поэтому Ord(Base)он не ограничен, 0..3но имеет те же ограничения, что и базовый числовой тип. Это выбор языка дизайна, компромисс между безопасностью и производительностью. При каждом касании Baseвведенного значения учитывайте «безопасные» проверки с ограничением по времени выполнения . Или как определить «значение переполнения» Baseзначения для арифметики и для ++и --? И т.д.
zzzz
7
В дополнение к jnml, даже семантически, ничто в языке не говорит о том, что константы, определенные как Base, представляют весь диапазон допустимых Base, это просто говорит о том, что эти конкретные const имеют тип Base. Больше констант также может быть определено в другом месте как Base, и это даже не является взаимоисключающим (например, const Z Base = 0 может быть определено и будет действительным).
МНА
10
Вы можете использовать, iota + 1чтобы не начинать с 0.
Marçal Juan
87

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

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
}

Внутри основной пакет a.Baserтеперь фактически похож на enum. Только внутри пакета вы можете определить новые экземпляры.

metakeule
источник
10
Ваш метод кажется идеальным для случаев, когда baseиспользуется только как приемник метода. Если бы ваш aпакет выставлял функцию, принимающую параметр типа base, это стало бы опасно. Действительно, пользователь может просто вызвать его с буквальным значением 42, которое будет принимать функция, baseпоскольку оно может быть преобразовано в int. Чтобы предотвратить это, сделать : . Проблема: вы больше не можете объявлять базы как константы, только переменные модуля. Но 42 никогда не будут брошены в этот тип. basestructtype base struct{value:int}base
Нириэль
6
@metakeule Я пытаюсь понять твой пример, но твой выбор имен переменных сделал его чрезвычайно трудным.
anon58192932
1
Это один из моих ошибок в примерах. ФГС, я понимаю, что это заманчиво, но не называйте переменную так же, как тип!
Грэм Николлс
27

Вы можете сделать это так:

type MessageType int32

const (
    TEXT   MessageType = 0
    BINARY MessageType = 1
)

С этим кодом компилятор должен проверять тип перечисления

Азат
источник
5
Константы обычно пишутся в обычном верблюжьем регистре, а не в верхнем. Начальная заглавная буква означает, что переменная экспортируется, что может или не может быть то, что вы хотите.
425nep
1
Я заметил, что в исходном коде Go есть смесь, в которой иногда все константы имеют верхний регистр, а иногда - верблюд. У вас есть ссылка на спецификацию?
Джереми Гэйлор
@JeremyGailor Я думаю, что 425nesp просто замечает, что разработчики обычно предпочитают использовать их как неэкспортированные константы, поэтому используйте camelcase. Если разработчик решает, что он должен быть экспортирован, тогда не стесняйтесь использовать все прописные или прописные буквы, потому что нет никаких установленных предпочтений. См. Раздел « Рекомендации по пересмотру кода
Голанга»
Есть предпочтение. Так же, как переменные, функции, типы и другие, имена констант должны быть mixedCaps или MixedCaps, а не ALLCAPS. Источник: Go Code Review Комментарии .
Родольфо Карвалью
Обратите внимание, что, например, функции, ожидающие MessageType, с радостью примут нетипизированные числовые константы, например 7. Кроме того, вы можете привести любой int32 к MessageType. Если вы знаете об этом, я думаю, что это самый идиоматический путь в ходу.
Коста
23

Это правда, что приведенные выше примеры использования constи iotaявляются наиболее идиоматическими способами представления примитивных перечислений в Go. Но что, если вы ищете способ создания более полнофункционального перечисления, подобного типу, который вы видели бы на другом языке, таком как Java или Python?

Очень простой способ создать объект, который начинает выглядеть и чувствовать себя как строковое перечисление в Python:

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

Предположим, вам также нужны некоторые служебные методы, например Colors.List(), и Colors.Parse("red"). И ваши цвета были более сложными и должны были быть структурой. Тогда вы можете сделать что-то вроде этого:

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

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

Бекка Петрин
источник
19

Начиная с Go 1.4, go generateинструмент был представлен вместе с stringerкомандой, которая делает ваш enum легко отлаживаемым и печатным.

Моше Рева
источник
Знаете ли вы, это противоположное решение. Я имею в виду строку -> MyType. Так как одностороннее решение далеко от идеального. Вот sb gist, который делает то, что я хочу - но писать вручную легко, чтобы сделать ошибки.
СР
11

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

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

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

Редактировать:

Добавление другого способа использования констант для перечисления

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
источник
2
Вы можете объявить константы со строковыми значениями. IMO это проще сделать, если вы собираетесь их отображать и вам не нужно числовое значение.
cbednarski
4

Вот пример, который окажется полезным, когда есть много перечислений. Он использует структуры в Голанге и опирается на объектно-ориентированные принципы, чтобы связать их вместе в аккуратный маленький пучок. Ни один из базового кода не изменится при добавлении или удалении нового перечисления. Процесс такой:

  • Определите структуру перечисления для enumeration items: EnumItem . Он имеет целочисленный и строковый тип.
  • Определите enumerationкак список enumeration items: Enum
  • Методы сборки для перечисления. Некоторые из них были включены:
    • enum.Name(index int): возвращает имя для указанного индекса.
    • enum.Index(name string): возвращает имя для указанного индекса.
    • enum.Last(): возвращает индекс и имя последнего перечисления
  • Добавьте свои определения перечисления.

Вот некоторый код:

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"}}}
Аарон
источник