Apa cara idiomatis untuk mewakili enum di Go?

522

Saya mencoba mewakili kromosom yang disederhanakan, yang terdiri dari basa N, yang masing-masing hanya bisa satu {A, C, T, G}.

Saya ingin memformalkan kendala dengan enum, tapi saya bertanya-tanya apa cara yang paling idiomatis untuk meniru enum di Go.

karbokation
sumber
4
Paket standar go mereka diwakili sebagai konstanta. Lihat golang.org/pkg/os/#pkg-constants
Denys Séguret
Terkait / kemungkinan duplikat Golang: Menciptakan Tipe Konstan dan Membatasi Nilai
icza
7
@icza Pertanyaan ini diajukan 3 tahun sebelumnya. Ini bukan duplikat dari yang itu, dengan asumsi panah waktu dalam urutan kerja.
carbocation
Lihatlah panduan utama untuk pergi enum .
Inanc Gumus

Jawaban:

658

Mengutip dari spesifikasi bahasa: Iota

Dalam deklarasi konstan, iota pengidentifikasi yang dideklarasikan sebelumnya mewakili konstanta bilangan bulat tak bertanda. Ini diatur ulang ke 0 setiap kali kata const yang dicadangkan muncul di sumber dan peningkatan setelah masing-masing ConstSpec. Ini dapat digunakan untuk membangun satu set konstanta terkait:

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)

Di dalam ExpressionList, nilai setiap iota adalah sama karena hanya bertambah setelah setiap 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
)

Contoh terakhir ini mengeksploitasi pengulangan implisit dari daftar ekspresi non-kosong terakhir.


Jadi kode Anda mungkin seperti

const (
        A = iota
        C
        T
        G
)

atau

type Base int

const (
        A Base = iota
        C
        T
        G
)

jika Anda ingin basis menjadi tipe terpisah dari int.

zzzz
sumber
16
contoh yang bagus (saya tidak ingat perilaku iota yang tepat - ketika itu bertambah - dari spec). Secara pribadi saya suka memberikan tipe ke enum, sehingga dapat diperiksa jenisnya ketika digunakan sebagai argumen, bidang, dll.
mna
16
@Jnml sangat menarik. Tapi saya agak kecewa bahwa pengecekan tipe statis tampaknya longgar, misalnya tidak ada yang mencegah saya menggunakan Pangkalan n ° 42 yang tidak pernah ada: play.golang.org/p/oH7eiXBxhR
Menghapus
4
Go tidak memiliki konsep tipe subrange numerik, seperti misalnya Pascal's, jadi Ord(Base)tidak terbatas pada 0..3tetapi memiliki batas yang sama dengan tipe numerik yang mendasarinya. Ini adalah pilihan desain bahasa, kompromi antara keselamatan dan kinerja. Pertimbangkan pemeriksaan batas waktu berjalan "aman" setiap kali ketika menyentuh Basenilai yang diketik. Atau bagaimana seseorang harus mendefinisikan perilaku 'luapan' Basenilai untuk aritmatika dan untuk ++dan --? Dll
zzzz
7
Untuk melengkapi pada jnml, bahkan secara semantik, tidak ada dalam bahasa yang mengatakan bahwa konstanta yang didefinisikan sebagai Base mewakili seluruh rentang Base yang valid, ia hanya mengatakan bahwa konstanta khusus ini adalah tipe Base. Lebih banyak konstanta dapat didefinisikan di tempat lain sebagai Basis juga, dan itu bahkan tidak eksklusif satu sama lain (misalnya const Z Basis = 0 dapat didefinisikan dan akan valid).
mna
10
Anda dapat menggunakan iota + 1untuk tidak memulai pada 0.
Marçal Juan
87

Mengacu pada jawaban jnml, Anda dapat mencegah instance baru tipe Base dengan tidak mengekspor tipe Base sama sekali (yaitu, menulisnya dengan huruf kecil). Jika perlu, Anda dapat membuat antarmuka yang dapat diekspor yang memiliki metode yang mengembalikan tipe dasar. Antarmuka ini dapat digunakan dalam fungsi-fungsi dari luar yang berhubungan dengan Basa, yaitu

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
}

Di dalam paket utama a.Baserefektif seperti enum sekarang. Hanya di dalam paket Anda dapat menentukan contoh baru.

metakeule
sumber
10
Metode Anda tampaknya sempurna untuk kasus-kasus di mana basehanya digunakan sebagai penerima metode. Jika apaket Anda mengekspos fungsi mengambil parameter tipe base, maka itu akan menjadi berbahaya. Memang, pengguna hanya bisa menyebutnya dengan nilai literal 42, yang fungsi akan terima basekarena dapat dicor ke int. Untuk mencegah hal ini, membuat basesebuah struct: type base struct{value:int}. Masalah: Anda tidak dapat mendeklarasikan basis sebagai konstanta lagi, hanya variabel modul. Tetapi 42 tidak akan pernah dilemparkan ke basetipe seperti itu.
Niriel
6
@metule Saya mencoba memahami contoh Anda, tetapi pilihan Anda dalam nama variabel membuatnya sangat sulit.
anon58192932
1
Ini adalah salah satu bugbears saya dalam contoh. FGS, saya sadar itu menggoda, tetapi jangan beri nama variabelnya sama dengan tipe!
Graham Nicholls
27

Anda bisa membuatnya:

type MessageType int32

const (
    TEXT   MessageType = 0
    BINARY MessageType = 1
)

Dengan kode ini kompiler harus memeriksa jenis enum

Azat
sumber
5
Konstanta biasanya ditulis dalam camelcase normal, tidak semua huruf besar. Huruf besar awal berarti bahwa variabel diekspor, yang mungkin atau mungkin tidak seperti yang Anda inginkan.
425nesp
1
Saya perhatikan dalam kode sumber Go ada campuran di mana kadang-kadang konstanta semua huruf besar dan kadang-kadang mereka adalah unta. Apakah Anda memiliki referensi ke spesifikasi?
Jeremy Gailor
@JeremyGailor Saya pikir 425nesp hanya mencatat bahwa preferensi normal adalah bagi pengembang untuk menggunakannya sebagai konstanta yang tidak diekspor, jadi gunakan camelcase. Jika pengembang menentukan bahwa itu harus diekspor maka jangan ragu untuk menggunakan semua huruf besar atau huruf kapital karena tidak ada preferensi yang ditetapkan. Lihat Rekomendasi Tinjauan Kode Golang dan Bagian Go Efektif tentang Konstanta
waynethec
Ada preferensi. Sama seperti variabel, fungsi, tipe, dan lainnya, nama konstan harus mixedCaps atau MixedCaps, bukan ALLCAPS. Sumber: Komentar Ulasan Kode Go .
Rodolfo Carvalho
Perhatikan bahwa mis. Fungsi yang mengharapkan MessageType akan dengan senang hati menerima konstanta numerik yang tidak diketik, misalnya 7. Selanjutnya, Anda dapat mengirimkan int32 apa pun ke MessageType. Jika Anda mengetahui hal ini, saya pikir ini adalah cara yang paling idiomatis.
Kosta
23

Memang benar bahwa contoh-contoh penggunaan di atas constdan iotamerupakan cara paling idiomatis untuk mewakili enum primitif di Go. Tetapi bagaimana jika Anda sedang mencari cara untuk membuat enum yang lebih berfitur lengkap mirip dengan jenis yang akan Anda lihat dalam bahasa lain seperti Java atau Python?

Cara yang sangat sederhana untuk membuat objek yang mulai terlihat dan terasa seperti string enum dengan Python adalah:

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

Misalkan Anda juga menginginkan beberapa metode utilitas, seperti Colors.List(), dan Colors.Parse("red"). Dan warna Anda lebih kompleks dan perlu menjadi struct. Maka Anda mungkin melakukan sesuatu yang sedikit seperti ini:

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

Pada titik itu, pasti berhasil, tetapi Anda mungkin tidak suka bagaimana Anda harus mendefinisikan warna secara berulang. Jika pada titik ini Anda ingin menghilangkan itu, Anda dapat menggunakan tag pada struct Anda dan melakukan beberapa perenungan untuk mengaturnya, tetapi mudah-mudahan ini cukup untuk menutupi sebagian besar orang.

Becca Petrin
sumber
19

Mulai Go 1.4, go generatealat ini telah diperkenalkan bersama dengan stringerperintah yang membuat enum Anda mudah disangkal dan dapat dicetak.

Moshe Revah
sumber
Apakah Anda tahu solusi yang tepat. Maksud saya string -> MyType. Karena solusi satu arah jauh dari ideal. Ini adalah inti yang melakukan apa yang saya inginkan - tetapi menulis dengan tangan mudah untuk membuat kesalahan.
SR
11

Saya yakin kami punya banyak jawaban bagus di sini. Tapi, saya hanya berpikir untuk menambahkan cara saya menggunakan tipe yang disebutkan

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

Sejauh ini, ini adalah salah satu cara idiomatis yang bisa kita buat dengan tipe yang ditentukan dan digunakan di Go.

Edit:

Menambahkan cara lain menggunakan konstanta untuk menghitung

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)

}
tongkat sihir
sumber
2
Anda bisa mendeklarasikan konstanta dengan nilai string. IMO lebih mudah untuk melakukannya jika Anda bermaksud untuk menampilkannya dan tidak benar-benar membutuhkan nilai numerik.
cbednarski
4

Berikut adalah contoh yang akan terbukti bermanfaat ketika ada banyak enumerasi. Ia menggunakan struktur di Golang, dan menggunakan Prinsip Berorientasi Objek untuk mengikat mereka semua dalam satu bungkusan kecil yang rapi. Tidak satu pun dari kode yang mendasarinya akan berubah ketika enumerasi baru ditambahkan atau dihapus. Prosesnya adalah:

  • Tentukan struktur enumerasi untuk enumeration items: EnumItem . Ini memiliki tipe integer dan string.
  • Tentukan enumerationsebagai daftar enumeration items: Enum
  • Membangun metode untuk pencacahan. Beberapa telah dimasukkan:
    • enum.Name(index int): mengembalikan nama untuk indeks yang diberikan.
    • enum.Index(name string): mengembalikan nama untuk indeks yang diberikan.
    • enum.Last(): mengembalikan indeks dan nama enumerasi terakhir
  • Tambahkan definisi enumerasi Anda.

Ini beberapa kode:

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