Go: 提案:规范:添加类型化枚举支持

创建于 2017-04-01  ·  180评论  ·  资料来源: golang/go

我想建议将枚举作为一种特殊的type添加到 Go 中。 下面的示例是从 protobuf 示例中借用的。

今天围棋中的枚举

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
}

语言支持的外观

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

这种模式很常见,我认为它需要特殊的大小写,并且我相信它使代码更具可读性。 在实现层,我想大多数情况可以在编译时检查,其中一些现在已经发生,而另一些几乎不可能或需要大量权衡。

  • 导出类型的安全性:没有什么能阻止某人做SearchRequest(99)SearchRequest("MOBILEAPP") 。 当前的解决方法包括使用选项制作未导出的类型,但这通常会使生成的代码更难使用/记录。
  • 运行时安全性:就像 protobuf 将在解组时检查有效性一样,这提供了语言范围的验证,只要枚举被实例化。
  • 工具/文档:今天的许多软件包都将有效选项放入字段注释中,但并非每个人都这样做,并且不能保证注释不会过时。

需要考虑的事情

  • Nil :通过在类型系统之上实现enum ,我认为这不需要特殊的大小写。 如果有人希望nil有效,则应将枚举定义为指针。
  • 默认值/运行时分配:这是更艰难的决定之一。 如果 Go 默认值未定义为有效枚举怎么办? 静态分析可以在编译时缓解其中的一些问题,但需要有一种方法来处理外部输入。

我对语法没有任何强烈的意见。 我相信这可以做得很好,并对生态系统产生积极影响。

Go2 LanguageChange NeedsInvestigation Proposal

最有用的评论

@md2perpe不是枚举。

  1. 它们不能被枚举、迭代。
  2. 它们没有有用的字符串表示。
  3. 他们没有身份:

```去
包主

进口 (
“fmt”
)

功能主要(){
类型 SearchRequest int
常量 (
通用搜索请求 = iota
网络
)

const (
    Another SearchRequest = iota
    Foo
)

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

}
````

我完全同意@derekperkins的观点,即 Go 需要一些枚举作为一等公民。 我不确定那会是什么样子,但我怀疑它可以在不破坏 Go 1 玻璃房的情况下完成。

所有180条评论

@derekparker在 #19412 中有关于提出 Go2 提案的讨论

我今天早些时候通读了一遍,但这似乎更侧重于有效类型,这里侧重于有效类型值。 也许这是该提议的一个子集,但也是对今天可以放入 Go 的类型系统的影响较小的更改。

枚举是 sum 类型的一种特殊情况,其中所有类型都相同,并且通过方法与每个类型关联一个值。 更多的类型,当然,但同样的效果。 无论如何,这将是一个或另一个,总和类型涵盖更多的领域,甚至总和类型也不太可能。 无论如何,由于 Go1 兼容性协议,在 Go2 之前什么都不会发生,因为这些提案至少需要一个新的关键字,如果它们中的任何一个被接受的话

很公平,但这些提议都没有违反兼容性协议。 有一种观点认为 sum 类型“太大”而无法添加到 Go1。 如果是这样的话,那么这个提议是一个有价值的中间立场,可以成为 Go2 中全和类型的垫脚石。

他们都需要一个新的关键字,它会破坏有效的 Go1 代码,使用它作为标识符

我认为这可以解决

新的语言功能需要引人注目的用例。 所有语言特性都是有用的,或者没有人会提出它们; 问题是:它们是否有用到足以证明语言复杂化并要求每个人都学习新概念? 这里有哪些引人注目的用例? 人们将如何使用这些? 例如,人们是否期望能够迭代一组有效的枚举值,如果是这样,他们将如何做到这一点? 这个提议是否不仅仅是让您避免向某些开关添加默认情况?

这是在当前 Go 中编写枚举的惯用方式:

type SearchRequest int

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

这样做的好处是很容易创建可以 OR:ed 的标志(使用运算符| ):

type SearchRequest int

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

我看不出引入关键字enum会使它更短。

@md2perpe不是枚举。

  1. 它们不能被枚举、迭代。
  2. 它们没有有用的字符串表示。
  3. 他们没有身份:

```去
包主

进口 (
“fmt”
)

功能主要(){
类型 SearchRequest int
常量 (
通用搜索请求 = iota
网络
)

const (
    Another SearchRequest = iota
    Foo
)

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

}
````

我完全同意@derekperkins的观点,即 Go 需要一些枚举作为一等公民。 我不确定那会是什么样子,但我怀疑它可以在不破坏 Go 1 玻璃房的情况下完成。

@md2perpe iota是一种非常有限的方法来处理枚举,它适用于有限的情况。

  1. 你需要一个int
  2. 你只需要在你的包内部保持一致,不代表外部状态

一旦你需要表示一个字符串或其他类型,这对于外部标志很常见, iota对你不起作用。 如果您想与外部/数据库表示进行匹配,我不会使用iota ,因为源代码中的排序很重要,并且重新排序会导致数据完整性问题。

这不仅仅是缩短代码的便利问题。 这是一项提案,它将以当今语言无法强制执行的方式实现数据完整性。

@ianlancetaylor

例如,人们是否期望能够迭代一组有效的枚举值,如果是这样,他们将如何做到这一点?

正如@bep 所提到的,我认为这是一个可靠的用例。 我认为迭代看起来像一个标准的 Go 循环,我认为它们会按照定义的顺序循环。

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

如果 Go 要添加的不仅仅是 iota,那么为什么不使用代数数据类型呢?

根据定义顺序扩展排序,并按照protobuf的例子,我认为该字段的默认值将是第一个定义的字段。

@bep不太方便,但您可以获得所有这些属性:

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

我认为编译时检查的枚举不是一个好主意。 我相信 go现在几乎有这个。 我的理由是

  • 对于添加或删除的情况,编译时检查的枚举既不向后也不向前兼容。 #18130 花费大量精力来实现逐步的代码修复; 枚举会破坏这种努力; 任何想要更改一组枚举的包都会自动并强制破坏其所有导入程序。
  • 与原始评论声称的相反,protobuf(出于该特定原因)实际上并未检查枚举字段的有效性。 proto2 指定枚举的未知值应被视为未知字段,并且 proto3 甚至指定生成的代码必须有一种用编码值表示它们的方法(就像 go 目前对假枚举所做的那样)
  • 最后,它实际上并没有增加很多。 您可以使用 stringer 工具进行字符串化。 您可以通过添加标记 MaxValidFoo const 来获得迭代(但请参阅上面的警告。您甚至不应该有这个要求)。 您首先不应该拥有两个 const-decl 。 只需将一个工具集成到您的 CI 中即可进行检查。
  • 我不相信除了整数之外的其他类型实际上是必要的。 stringer 工具应该已经涵盖了字符串的转换; 最后,生成的代码将等同于编译器无论如何都会生成的代码(除非您认真建议对“字符串枚举”的任何比较都会迭代字节......)

总的来说,对我来说只是一个巨大的-1。 它不仅没有添加任何东西; 它积极地伤害。

我认为 Go 中当前的枚举实现非常简单,并且提供了足够的编译时间检查。 我实际上期待某种具有基本模式匹配的 Rust 枚举,但它可能会破坏 Go1 的保证。

由于枚举是 sum 类型的特例,而普遍的看法是我们应该使用接口来模拟 sum 类型,答案显然是https://play.golang.org/p/1BvOakvbj2

(如果不清楚:是的,那是个笑话——在经典的程序员时尚中,我差一点)。

严肃地说,对于这个线程中讨论的特性,一些额外的工具会很有用。

与 stringer 工具一样,“游侠”工具可以在我上面链接的代码中生成Iter函数的等效项。

有些东西可以生成 {Binary,Text}{Marshaler,Unmarshaler} 实现,以使它们更容易通过网络发送。

我敢肯定有很多像这样的小东西有时会非常有用。

有一些审查/linter 工具可以对使用接口模拟的总和类型进行详尽检查。 iota 枚举没有理由不能告诉您何时丢失案例或使用无效的无类型常量(也许它应该只报告 0 以外的任何内容?)。

即使没有语言变化,这方面肯定还有改进的空间。

枚举将补充已经建立的类型系统。 正如本期中的许多示例所示,枚举的构建块已经存在。 正如通道是建立在更多原语类型上的高级抽象一样,枚举也应该以相同的方式构建。 人类是傲慢、笨拙和健忘的,枚举之类的机制可以帮助人类程序员减少编程错误。

@bep我不得不不同意你的所有三个观点。 Go 惯用的枚举与 C 的枚举非常相似,它们没有任何有效值的迭代,没有任何自动转换为字符串,并且不一定具有不同的标识。

迭代很好,但在大多数情况下,如果您想要迭代,可以为第一个和最后一个值定义常量。 您甚至可以在添加新值时以不需要更新的方式执行此操作,因为iota会自动使其成为过去式。 语言支持会产生有意义的差异的情况是枚举的值是不连续的。

自动转换为字符串只是一个很小的值:特别是在这个提议中,字符串值需要写入到与 int 值相对应的位置,因此自己显式地写入字符串值数组几乎没有什么好处。 在另一个建议中,它可能更有价值,但强制变量名称对应于字符串表示也有缺点。

最后,我什至不确定独特的身份是否是一个有用的功能。 枚举不是像 Haskell 中那样的求和类型。 它们被命名为数字。 例如,使用枚举作为标志值是很常见的。 例如,您可以拥有ReadWriteMode = ReadMode | WriteMode ,这是一个有用的东西。 很有可能还有其他值,例如你可能有DefaultMode = ReadMode 。 在任何情况下,都没有任何方法可以阻止某人写const DefaultMode = ReadMode ; 要求它在单独的声明中发生的目的是什么?

@bep我不得不不同意你的所有三个观点。 Go 惯用的枚举与 C 的枚举非常相似,它们没有任何有效值的迭代,没有任何自动转换为字符串,并且不一定具有不同的标识。

@alercah ,请不要将此idomatic Go作为所谓的“获胜论点”拉入任何讨论; Go 没有内置的枚举,所以谈论一些不存在的 idoms 没有什么意义。

Go 被构建为更好的 C/C++更简洁的 Java ,因此将其与后者进行比较会更有意义。 而且 Java 确实有一个内置的Enum type (“Java 编程语言枚举类型比其他语言中的对应物强大得多。”): https ://docs.oracle.com/javase/tutorial/java

而且,虽然您可能不同意“更强大的部分”,但 Java Enum类型确实具有我提到的所有三个特性。

我可以理解 Go 更精简、更简单等的论点,并且必须采取一些妥协来保持这种方式,而且我在这个线程中看到了一些 hacky 变通方法,这种方法有效,但是一组iota ints 并不单独进行枚举。

枚举和自动字符串转换是“go generate”功能的理想选择。 我们已经有了一些解决方案。 Java 枚举介于经典枚举和总和类型之间。 所以在我看来这是一个糟糕的语言设计。
惯用的 Go 是关键,我认为没有充分的理由将语言 X 的所有功能复制到语言 Y,只是因为有人熟悉。

Java 编程语言枚举类型比其他语言中的枚举类型强大得多

十年前确实如此。 请参阅由总和类型和模式匹配支持的 Rust 中 Option 的现代零成本实现。

惯用的 Go 是关键,我认为没有充分的理由将语言 X 的所有功能复制到语言 Y,只是因为有人熟悉。

请注意,我不太同意这里给出的结论,但是使用 _ 惯用的 Go_ 将 Go 放在了一些艺术的基础上。 大多数软件编程都是相当无聊和实用的。 通常你只需要用一个枚举填充一个下拉框......

//go:generate enumerator Foo,Bar
一次编写,随处可用。 请注意,该示例是抽象的。

@bep我认为您误读了原始评论。 我相信“Go idiomatic enums”应该是指当前使用类型 Foo int + const-decl + iota 的构造,而不是说“无论你提出什么都不是惯用的”。

@rsc关于Go2标签,这与我提交此提案的理由背道而驰。 #19412 是一个完整的类型提案,它是一个比我这里的简单枚举提案更强大的超集,我宁愿在 Go2 中看到它。 在我看来,Go2 在未来 5 年内发生的可能性很小,我宁愿看到一些事情在更短的时间内发生。

如果我提出的新保留关键字enum对 BC 来说是不可能的,那么还有其他方法可以实现它,无论是全面的语言集成还是go vet内置的工具。 就像我最初所说的那样,我并不特别关注语法,但我坚信它对今天的 Go 来说是一个有价值的补充,而不会给新用户增加显着的认知负担。

在 Go 2 之前不可能有新的关键字。这显然违反了 Go 1 的兼容性保证。

就个人而言,我还没有看到 enum 的令人信服的论点,或者就此而言,对于 sum 类型,甚至对于 Go 2。我并不是说它们不会发生。 但是 Go 语言的目标之一是语言的简单性。 语言功能仅仅有用是不够的; 所有语言特性都是有用的——如果它们没有用,没有人会提出它们。 为了给 Go 添加一个特性,这个特性必须有足够多的引人入胜的用例,以使它值得使语言复杂化。 最引人注目的用例是没有该功能就无法编写的代码,至少现在没有很大的尴尬。

我很想在 Go 中看到枚举。 我经常发现自己想要限制我的公开 API(或在我的应用程序之外使用受限 API),其中有效输入的数量有限。 对我来说,这是一个枚举的完美地点。

例如,我可以制作一个连接到某种 RPC 样式 API 的客户端应用程序,并具有一组指定的操作/操作码。 我可以为此使用const s,但是没有什么可以阻止任何人(包括我自己!)只是发送无效代码。

另一方面,如果我正在为同一个 API 编写服务器端,那么能够在枚举上编写一个 switch 语句会很好,这会引发编译器错误(或至少一些go vet警告)如果未检查枚举的所有可能值(或至少存在default: )。

我认为这个(枚举)是 Swift真正做对的领域。

我可以制作一个连接到某种 RPC 样式 API 的客户端应用程序,并具有一组指定的操作/操作码。 我可以为此使用 const,但没有什么可以阻止任何人(包括我自己!)只是发送无效代码。

这是一个用枚举解决的可怕想法。 这意味着您现在永远无法添加新的枚举值,因为突然 RPC 可能会失败,或者您的数据在回滚时将变得不可读。 proto3要求生成的 enum-code 支持“未知代码”值的原因是,这是从痛苦中吸取的教训(与proto2如何解决这个问题进行比较,后者更好,但仍然很糟糕)。 您希望应用程序能够优雅地处理这种情况。

@Merovius我尊重你的意见,但礼貌地不同意。 确保只使用有效值是枚举的主要用途之一。

枚举并不适合所有情况,但它们对某些情况非常有用! 在大多数情况下,正确的版本控制和错误处理应该能够处理新值。

当然,处理具有 uh-oh 状态的外部进程是必须的。

使用枚举(或更通用和有用的 sum 类型),您可以向编译器强制您处理的 sum/enum 添加显式“未知”代码(或者如果您能做的只是在端点处完全处理这种情况)记录它并继续下一个请求)。

当我知道必须处理 X 个案例时,我发现 sum 类型在流程内部更有用。 对于小 X,管理起来并不难,但对于大 X,我很欣赏编译器对我大喊大叫,尤其是在重构时。

跨 API 边界的用例更少,而且人们应该总是在可扩展性方面犯错,但有时你确实有一些东西确实只能是 X 东西之一,比如 AST 或更琐碎的例子,比如“day of周”值,此时范围几乎已确定(取决于日历系统的选择)。

@jimmyfrasche我可能会给你星期几,但不是 AST。 语法不断发展。 今天可能无效的内容,明天可能完全有效,这可能涉及向 AST 添加新的节点类型。 使用经过编译器检查的总和类型,如果没有损坏,这将是不可能的。

而且我不明白为什么这不能通过兽医检查来解决; 为您提供完全合适的静态检查,并为我提供逐步修复的可能性。

我正在为服务器 API 实现客户端。 一些参数和返回值是 API 中的枚举。 总共有 45 种枚举类型。

在我的情况下,在 Go 中使用枚举常量是不可行的,因为不同枚举类型的一些值共享相同的名称。 在下面的示例中, Destroy出现了两次,因此编译器将发出错误Destroy redeclared in this block

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

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

因此,我需要提出不同的表示。 理想情况下,它允许 IDE 显示给定类型的可能值,以便客户端用户可以更轻松地使用它。 将枚举作为 Go 中的一等公民将满足这一点。

@kongslund我知道这不是一个完美的实现,但我只是制作了一个您可能感兴趣的代码生成器。 它只要求您在类型声明上方的注释中声明您的枚举,并为您生成其余部分。

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

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

会产生

const(
  _ TaskAllowedOperations = iota
  TaskAllowedOperationsCancel
  TaskAllowedOperationsDestroy
)

const(
  _ OnNormalExit = iota
  OnNormalExitDestroy
  OnNormalExitRestart
)

更好的部分是它会生成排除前缀的String()方法,允许您将"Destroy"解析为TaskAllowedOperationsOnNormalExit

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

现在插头已经不碍事了...

我个人不介意枚举不包含在 go 语言中,这不是我对此事的最初感受。 第一次来的时候,我经常对为什么做出这么多选择有一个困惑的反应。 但是在使用该语言之后,很高兴拥有它所坚持的简单性,如果需要额外的东西,很可能其他人也需要它并制作了一个很棒的包来帮助解决这个特定问题。 保持我的自由裁量权。

在这次讨论中提出了许多有效的观点,一些赞成枚举支持,也有许多反对它(至少就提案所说的“枚举”是什么而言)。 一些对我来说很突出的事情:

  • 介绍性示例(今天的 Go 中的枚举)具有误导性:该代码是生成的,几乎没有人会手动编写这样的 Go 代码。 事实上,这个建议(在语言支持下可能看起来如何)更接近于我们在 Go 中实际已经做的事情。

  • @jediorange提到 Swift“确实(枚举)是正确的”:尽管如此,但 Swift 枚举是一个令人惊讶的复杂野兽,将各种概念混合在一起。 在 Go 中,我们故意避免与其他语言特征重叠的机制,从而获得更多的正交性。 程序员的结果是她不必决定使用哪个特性:枚举或类,或求和类型(如果我们有它们),或接口。

  • @ianlancetaylor关于语言功能有用性的观点不能掉以轻心。 有无数有用的功能; 问题是哪些是真正令人信服的并且值得付出代价(语言的额外复杂性,因此可读性和实现)。

  • 作为一个小问题,Go 中 iota 定义的常量当然不限于 int。 只要它们是常量,它们就被限制为(可能命名为)基本类型(包括浮点数、布尔值、字符串:https://play.golang.org/p/lhd3jqqg5z)。

  • @merovius很好地说明了(静态!)编译时检查的局限性。 我非常怀疑无法扩展的枚举是否适用于需要或预期扩展的情况(任何长期存在的 API 表面都会随着时间的推移而演变)。

这让我想到了一些关于这个提案的问题,我认为在取得任何有意义的进展之前需要回答这些问题:

1) 提议的枚举的实际期望是什么? @bep提到了可枚举性、可迭代性、字符串表示、身份。 还有更多吗? 有没有少?

2)假设1)中的列表,可以扩展枚举吗? 如果是这样,怎么做? (在同一个包中?另一个包?)如果它们不能扩展,为什么不呢? 为什么在实践中这不是问题?

3) 命名空间:在 Swift 中,枚举类型引入了一个新的命名空间。 有重要的机制(语法糖,类型推导),因此命名空间名称不必在任何地方重复。 例如,对于枚举 Month 的枚举值,在正确的上下文中,可以编写 .January 而不是 Month.January(或更糟的是,MyPackage.Month.January)。 是否需要枚举命名空间? 如果是这样,枚举命名空间如何扩展? 在实践中进行这项工作需要什么样的语法糖?

4)枚举值是常量吗? 不变的价值观?

5)枚举值可以进行哪些操作(例如,除了迭代之外):我可以向前移动一个,向后移动一个吗? 它是否需要额外的内置函数或运算符? (并非所有迭代都按顺序排列)。 如果一个人向前移动超过最后一个枚举值会发生什么? 那是运行时错误吗?

(我已在 https://github.com/golang/go/issues/19814#issuecomment-322771922 中更正了我对下一段的措辞。为以下粗心的措辞道歉。)

如果不试图真正回答这些问题,这个提议是没有意义的(“我想要做我想做的事情的枚举”不是一个提议)。

如果不试图真正回答这些问题,这个提议是没有意义的

@griesemer您有很多要点/问题-但是由于不回答这些问题而将这个提议标记为毫无意义是没有意义的。 这个项目的贡献门槛很高,但它应该被允许在没有编译器博士学位的情况下_提出一些东西_,并且一个提案不应该是一个_准备好实施_的设计。

  • Go 需要这个提案,因为它开始了急需的讨论 => 价值和意义
  • 如果它也导致提案#21473 => 价值和意义

介绍性示例(今天的 Go 中的枚举)具有误导性:该代码是生成的,几乎没有人会手动编写这样的 Go 代码。 事实上,这个建议(在语言支持下可能看起来如何)更接近于我们在 Go 中实际已经做的事情。

@griesemer我不得不不同意。 我不应该在 Go 变量名中保留完整的大写字母,但是在很多地方,手写代码看起来几乎与我的建议相同,这些代码是由我在 Go 社区中尊敬的 Google 员工编写的。 我们在代码库中经常遵循相同的模式。 这是从 Google Cloud Go 库中提取的示例。

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

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

他们在多个地方使用相同的构造。
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

稍后有一些讨论,如果您可以使用iota ,如何使事情变得更简洁,这本身可能很有用,但对于有限的用例。 有关更多详细信息,请参阅我之前的评论。 https://github.com/golang/go/issues/19814#issuecomment -290948187

@bep公平点; 我为我粗心的措辞道歉。 让我再试一次,希望这次能更尊重和更清楚地表达我上面的最后一段:

为了能够取得有意义的进展,我认为该提案的支持者应该尝试更准确地说明他们认为枚举的重要特征(例如通过回答 https://github.com 中的一些问题)。 com/golang/go/issues/19814#issuecomment-322752526)。 从到目前为止的讨论来看,所需的功能只是描述得相当模糊。

也许作为第一步,进行案例研究来展示现有 Go 如何(显着)不足以及枚举如何更好/更快/更清晰地解决问题等会非常有用。另请参阅@rsc在 Gophercon 上的精彩演讲关于 Go2 语言的变化。

@derekperkins我会称这些(类型化的)常量定义,而不是枚举。 我猜我们的分歧是由于对“枚举”应该是什么的不同理解,因此我的问题在上面。

(我之前的 https://github.com/golang/go/issues/19814#issuecomment-322774830 当然应该去@derekperkins ,而不是@derekparker。自动完成打败了我。)

@derekperkins 的评论和部分回答我自己的问题来看,我认为 Go 的“枚举”应该至少具有以下品质:

  • 将一组值分组到(新)类型下的能力
  • 使用该类型轻松声明名称(以及相应的值,如果有),语法开销或样板文件最少
  • 能够以声明的升序遍历这些值

听起来对吗? 如果是这样,还有什么需要添加到此列表中?

你的问题都是好问题。

提议的枚举的实际期望是什么? @bep提到了可枚举性、可迭代性、字符串表示、身份。 还有更多吗? 有没有少?

假设 1) 中的列表,可以扩展枚举吗? 如果是这样,怎么做? (在同一个包中?另一个包?)如果它们不能扩展,为什么不呢? 为什么在实践中这不是问题?

我不认为枚举可以扩展有两个原因:

  1. 枚举应该代表可接受值的全部范围,因此扩展它们没有意义。
  2. 就像普通的 Go 类型不能在外部包中扩展一样,这保持了相同的机制和开发人员的期望

命名空间:在 Swift 中,枚举类型引入了一个新的命名空间。 有重要的机制(语法糖,类型推导),因此命名空间名称不必在任何地方重复。 例如,对于枚举 Month 的枚举值,在正确的上下文中,可以编写 .January 而不是 Month.January(或更糟的是,MyPackage.Month.January)。 是否需要枚举命名空间? 如果是这样,枚举命名空间如何扩展? 在实践中进行这项工作需要什么样的语法糖?

我了解命名空间是如何产生的,因为我提到的所有示例都以类型名称为前缀。 如果有人强烈反对添加命名空间,我不会反对,但我认为这超出了本提案的范围。 前缀适合当前系统。

枚举值是常量吗? 不变的价值观?

我会认为常数。

枚举值可以进行哪些操作(例如,除了迭代之外):我可以向前移动一个,向后移动一个吗? 它是否需要额外的内置函数或运算符? (并非所有迭代都按顺序排列)。 如果一个人向前移动超过最后一个枚举值会发生什么? 那是运行时错误吗?

我会默认使用切片/数组(不是地图)的标准 Go 实践。 枚举值将根据声明顺序进行迭代。 至少会有范围支持。 我倾向于不让通过索引访问枚举,但对此并不强烈。 不支持应该消除潜在的运行时错误。

将无效值分配给枚举会导致新的运行时错误(恐慌?),无论是通过直接分配还是类型转换。

如果我正确地总结了这一点,那么您建议的枚举值就像类型化的常量(并且像常量一样,它们可能具有用户定义的常量值)但是:

  • 他们还在同一个声明中定义了与枚举值关联的枚举类型(否则它们只是常量)
  • 不可能在其声明之外强制转换/创建现有枚举类型的枚举值
  • 可以迭代它们

那个听起来是对的吗? (这与语言对枚举采取的经典方法相匹配,大约 45 年前由 Pascal 开创)。

是的,这正是我所提议的。

开关语句呢? AIUI 是该提案的主要推动力之一。

我认为,能够打开枚举是隐含的,因为您基本上可以打开任何东西。 如果您没有完全满足开关中的枚举,我确实喜欢 swift 有错误,但这可以由 vet 处理

@jediorange我特别指的是最后一部分的问题,即是否应该进行详尽的检查(为了保持提案的完整性)。 “不”当然是一个完美的答案。

此问题的原始消息提到 protobufs 作为激励因素。 我想用现在给出的语义明确指出,protobuf 编译器需要为任何枚举创建一个额外的“无法识别”的情况(暗示一些名称修改方案以防止冲突)。 如果解码的枚举值不在编译范围内,它还需要使用枚举向任何生成的结构添加一个附加字段(同样,以某种方式修改名称)。 就像目前为java所做的一样。 或者,可能更有可能继续使用int s。

@Merovius我最初的提案提到了 protobufs 作为一个例子,而不是作为提案的主要动机。 你提出了一个关于这种整合的好观点。 我认为它可能应该被视为一个正交问题。 我见过的大多数代码都从生成的 protobuf 类型转换为应用级结构,更喜欢在内部使用这些结构。 对我来说,protobuf 可以继续保持不变是有意义的,如果应用程序创建者想要将它们转换为 Go 枚举,他们可以处理你在转换过程中提出的边缘情况。

@derekperkins 还有一些问题:

  • 未显式初始化的枚举类型变量的零值是多少? 我认为它通常不能为零(这会使内存分配/初始化复杂化)。

  • 我们可以用枚举值做有限的算术吗? 例如,在 Pascal 中(我在其中编程过一次,很久以前),令人惊讶的是,经常需要以大于 1 的步数进行迭代。有时想要计算枚举值。

  • 关于迭代,为什么 go generate 产生的迭代(和字符串化)支持不够好?

未显式初始化的枚举类型变量的零值是多少? 我认为它通常不能为零(这会使内存分配/初始化复杂化)。

正如我在最初的提案中提到的,这是需要做出的更棘手的决定之一。 如果定义顺序对迭代很重要,那么我认为将第一个定义的值作为默认值同样有意义。

我们可以用枚举值做有限的算术吗? 例如,在 Pascal 中(我在其中编程过一次,很久以前),令人惊讶的是,经常需要以大于 1 的步数进行迭代。有时想要计算枚举值。

无论您使用的是数字还是基于字符串的枚举,这是否意味着所有枚举都具有基于零的隐式索引? 我之前提到我倾向于只支持range迭代而不是基于索引的原因是它不公开底层实现,它可以使用数组或映射或下面的任何东西。 我预计不需要通过索引访问枚举,但如果您有理由认为这样做会有所帮助,我认为没有理由禁止它。

关于迭代,为什么 go generate 产生的迭代(和字符串化)支持不够好?

迭代不是我个人的主要用例,尽管我确实认为它为提案增加了价值。 如果这是驱动因素,也许go generate就足够了。 这无助于保证价值安全。 Stringer()参数假定原始值将是iotaint或其他表示“真实”值的类型。 您还必须生成(Un)MarshalJSON(Un)MarshalBinaryScanner/Valuer以及您可能使用的任何其他序列化方法,以确保Stringer值用于与Go 内部使用的任何东西。

@griesemer我想我可能没有完全回答你关于枚举可扩展性的问题,至少在添加/删除值方面。 能够编辑它们是该提案的重要组成部分。

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

任何想要更改一组枚举的包都会自动并强制破坏其所有进口商

我看不出这与任何其他破坏性 API 更改有何不同。 由包的创建者负责处理 BC,就像类型、函数或函数签名发生变化一样。

从实现的角度来看,支持默认值不是全零的类型将非常复杂。 今天没有这样的类型。 要求这样的功能将被视为反对这个想法的标志。

该语言需要make来创建频道的唯一原因是为频道类型保留此功能。 否则make可能是可选的,仅用于设置通道缓冲区大小或将新通道分配给现有变量。

@derekperkins可以编排大多数其他 API 更改以进行逐步修复。 我真的建议阅读 Russ Cox 的描述,它让很多事情变得非常清楚。

开放枚举(如当前的 const+iota 构造)允许逐步修复,通过(例如)a)定义新值而不使用它,b)更新反向依赖关系以处理新值,c)开始使用该值。 或者,如果要删除一个值,a) 停止使用该值,b) 更新反向依赖项以不提及要删除的值,c) 删除该值。

使用封闭的(编译器检查是否详尽)枚举,这是不可能的。 如果您删除对值的处理或定义新值,编译器将立即抱怨缺少 switch-case。 而且您不能在定义一个值之前添加对值的处理。

问题不在于单个更改是否可以被认为是破坏性的(它们可以,孤立地),而是关于分布式代码库上是否存在不破坏性的提交序列。

从实现的角度来看,支持默认值不是全零的类型将非常复杂。 今天没有这样的类型。 要求这样的功能将被视为反对这个想法的标志。

@ianlancetaylor我绝对不能谈论完整的实现,但是如果枚举被实现为一个基于 0 的数组(这听起来像@griesemer所支持的),那么 0 作为索引看起来像它可以兼作“所有位为零”。

使用封闭的(编译器检查是否详尽)枚举,这是不可能的。

@Merovius如果go vet@jediorange建议的类似工具与编译器强制执行的类似工具检查了详尽性,那会减轻您的担忧吗?

@derekperkins关于它们的危害,是的。 不是因为它们缺乏用处。 大多数通常考虑使用它们的用例(系统调用、网络协议、文件格式、共享对象……)也会出现相同的版本偏差问题。 proto3 需要开放枚举而 proto2 不需要开放枚举是有原因的——这是从许多中断和数据损坏事件中吸取的教训。 尽管谷歌已经非常小心地避免了版本偏差。 从我的角度来看,带有默认情况的开放枚举只是正确的解决方案。 据我所知,除了所谓的针对无效值的安全性之外,它们并没有真正带来很多好处。

说了这么多,我不是决定者。

@derekperkinshttps://github.com/golang/go/issues/19814#issuecomment -322818206 中,您正在确认(从您的角度来看):

  • 枚举声明与命名枚举值(常量)一起声明枚举类型
  • 可以迭代它们
  • 不能在声明之外的枚举中添加任何值
    后来:切换枚举必须(或者可能不是)详尽无遗(似乎不太重要)

https://github.com/golang/go/issues/19814#issuecomment -322895247 你说:

  • 第一个定义的值可能应该是默认(零)值(请注意,这对迭代无关紧要,它对枚举变量初始化很重要)
  • 迭代不是你的主要动机

https://github.com/golang/go/issues/19814#issuecomment -322903714 中,您说“编辑它们的能力是本提案的重要组成部分”。

我很困惑:所以迭代不是主要的动力,很好。 这至少留下一个枚举值的枚举声明,它们是常量,并且它们不能在声明之外扩展。 但是现在您说编辑它们的能力很重要。 那是什么意思? 当然不是说它们可以扩展(这将是一个矛盾)。 它们是变量吗? (但它们不是常数)。

https://github.com/golang/go/issues/19814#issuecomment -322903714 中,您说枚举可以实现为基于 0 的数组。 这表明枚举声明引入了一种新类型以及枚举名称的有序列表,这些枚举名称是基于 0 的常量索引到枚举值数组中(自动为其保留空间)。 你是这个意思吗? 如果是这样,为什么仅仅声明一个固定大小的数组和一个常量索引列表就不够了? 数组边界检查将自动确保您不能“扩展”枚举范围,并且已经可以进行迭代。

我错过了什么?

我很困惑:所以迭代不是主要的动力,很好。

我有我自己想要枚举的原因,同时也试图考虑这个线程中的其他人,包括@bep和其他人,已经表达了作为提案的必要部分。

这至少留下一个枚举值的枚举声明,它们是常量,并且它们不能在声明之外扩展。 但是现在您说编辑它们的能力很重要。 那是什么意思? 当然不是说它们可以扩展(这将是一个矛盾)。 它们是变量吗? (但它们不是常数)。

当我说要编辑它们时, @Merovius的观点是它们是开放的枚举。 构建时的常量,但不会永远锁定。

在#19814(评论)中,您说枚举可以实现为基于 0 的数组。

这只是我根据您的https://github.com/golang/go/issues/19814#issuecomment -322884746 和@ianlancetaylorhttps 推测我如何想象它可能在幕后实施的超出我的工资等级: //github.com/golang/go/issues/19814#issuecomment -322899668

“我们可以对枚举值进行有限的算术运算吗?例如,在 Pascal 中(我在其中编程过一次,很久以前),令人惊讶的是,经常需要以大于 1 的步数进行迭代。有时人们想计算枚举值。”

我不知道您将如何计划对任何非整数枚举执行此操作,因此我的问题是该算法是否需要根据声明顺序为枚举的每个成员隐式分配一个索引。

从实现的角度来看,支持默认值不是全零的类型将非常复杂。 今天没有这样的类型。 要求这样的功能将被视为反对这个想法的标志。

同样,我不知道编译器是如何工作的,所以我只是想继续讨论。 归根结底,我并不想提出任何激进的建议。 就像您之前提到的那样,“这将符合语言对枚举采取的经典方法,大约 45 年前由 Pascal 开创”,这符合要求。

对于其他表示有兴趣的人,请随时参与。

另一个问题是是否可以使用这些枚举来索引数组或切片。 我认为,切片通常是表示枚举-> 值映射的一种非常有效且紧凑的方式,并且需要映射将是不幸的。

@derekperkins好的,我担心这会让我们(或至少是我)回到第一方:您要解决的问题是什么? 您是否只是想要一种更好的方法来编写我们目前对常量和 iota 所做的事情(为此我们使用 go generate 来获取字符串表示形式)? 也就是说,一些你(也许)觉得过于繁琐的符号的语法糖? (这是一个很好的答案,只是试图理解。)

您提到您有自己想要它们的原因,也许您可​​以多解释一下这些原因是什么。 你一开始给出的例子对我来说没有多大意义,但我可能遗漏了一些东西。

就目前而言,每个人对这个提议(“枚举”)的含义都有一些不同的理解,从各种回应中可以清楚地看出:Pascal 枚举和 Swift 枚举之间存在着巨大的可能性。 除非您(或其他人)非常清楚地描述了提议的内容(请注意,我并不是要求实施),否则很难取得任何有意义的进展,甚至只是辩论该提议的优点。

那有意义吗?

@griesemer这完全有道理,我理解@rsc 在Gophercon谈到的要通过的标准。 你显然比我有更深刻的理解。 在 #21473 中,您提到未实现 iota for vars 是因为当时没有令人信服的用例。 这与从一开始就没有包含enum的原因相同吗? 我很想知道你对 Go 是否会增加价值的看法,如果会,你会从哪里开始这个过程?

@derekperkins关于您在https://github.com/golang/go/issues/19814#issuecomment -323144075 中的问题:当时(在 Go 的设计中)我们只考虑相对简单的(比如 Pascal 或 C 风格)枚举。 我不记得所有细节,但肯定有一种感觉,枚举所需的额外机制没有足够的好处。 我们认为它们本质上是美化的不断声明。

这些传统枚举也存在问题:可以用它们进行算术运算(它们只是整数),但是如果它们“超出(枚举)范围”意味着什么? 在 Go 中,它们只是常量,不存在“超出范围”。 另一个是迭代:在 Pascal 中有特殊的内置函数(我认为是 SUCC 和 PRED)来向前和向后推进枚举类型变量的值(在 C 中只做 ++ 或 --)。 但是这里也出现了同样的问题:如果超过结尾会发生什么(在使用 ++ 或 Pascal 等效 SUCC 对枚举值进行范围的 for 循环中非常常见的问题)。 最后,枚举声明引入了一种新类型,其元素是枚举值。 这些值具有名称(在 enum 声明中定义的名称),但这些名称(在 Pascal,C 中)与类型在同一范围内。 这有点不令人满意:当声明两个不同的枚举时,人们希望可以为每个枚举类型使用相同的枚举值名称而不会发生冲突,这是不可能的。 当然 Go 也没有解决这个问题,但是一个常量声明看起来也不像是在引入一个新的命名空间。 一个更好的解决方案是在每次枚举时引入一个命名空间,但是每次使用枚举值时,都需要使用枚举类型名称进行限定,这很烦人。 Swift 通过在可能的情况下推断枚举类型来解决这个问题,然后可以使用以点为前缀的枚举值名称。 但这是相当多的机器。 最后,有时(通常在公共 API 中)需要扩展枚举声明。 如果这是不可能的(你不拥有代码),那就有问题了。 有了常数,这些问题就不存在了。

可能还有更多。 这就是我想到的。 最后,我们决定最好使用我们已经拥有的正交工具在 Go 中模拟枚举:自定义整数类型,使错误分配的可能性更小,以及用于语法糖的 iota 机制(以及避免重复初始化表达式的能力)。

因此我的问题是:您希望从专门的枚举声明中获得什么,我们无法在 Go 中以很少的语法开销充分模拟这些声明? 我可以想到枚举,以及不能在声明之外扩展的枚举类型。 我可以想到更多枚举值的能力,就像在 Swift 中一样。

枚举可以通过 Go 中的 go 生成器轻松解决。 我们已经有了纵梁。 跨 API 边界限制扩展是有问题的。 枚举值的更多功能(例如在 Swift 中)似乎与 Go 非常不同,因为它混合了许多正交概念。 在 Go 中,我们可能会通过使用基本构建块来实现这一点。

@griesemer感谢您周到的回复。 我不同意它们基本上是美化的常量声明。 在 Go 中具有类型安全性非常好, enum为我个人提供的主要价值是价值安全性。 今天在 Go 中模仿的方法是在该变量的每个入口点运行验证函数。 它很冗长,很容易出错,但使用今天的语言是可能的。 我已经通过在枚举前面为类型名称添加前缀来命名空间,这虽然很冗长,但没什么大不了的。

我个人不喜欢iota的大多数用途。 虽然很酷,但大多数时候我的enum类值映射到外部资源,如 db 或外部 api,我更愿意更明确地说,如果您碰巧重新排序,则不应更改该值。 iota对于我将使用enum的大多数地方也无济于事,因为我将使用字符串值列表。

归根结底,我不知道我还能澄清多少。 如果他们以任何对 Go 有意义的方式得到支持,我会很高兴。 不管具体的实现如何,我仍然可以使用它们,它们会使我的代码更安全。

我认为 Go 今天进行枚举的规范方式(如 https://github.com/golang/go/issues/19814#issuecomment-290909885 所示)非常接近正确。
有几个缺点:

  1. 它们不能被迭代
  2. 它们没有字符串表示
  3. 客户可以引入虚假的枚举值

没有#1我很好。
go:generate + stringer 可用于#2。 如果这不能处理您的用例,请将“枚举”的基本类型设为字符串而不是 int,并使用字符串常量值。

3 是当今 Go 难以处理的问题。 我有一个愚蠢的提议可以很好地处理这个问题。

explicit关键字添加到类型定义中。 此关键字禁止转换为此类型,但定义该类型的包中的 const 块中的转换除外。 (或者restricted ?或者enum意味着explicit type ?)

重复使用我上面引用的示例,

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

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

const块内有从intSearchRequest的转换。 但是只有包作者可以引入一个新的 SearchRequest 值,并且不太可能意外地引入一个(例如,通过将int传递给需要SearchRequest的函数)。

我并没有真正积极地提出这个解决方案,但我认为不要意外地构造一个无效的一个是今天无法在 Go 中捕获的枚举的显着属性(除非你用一个未出口的野外路线)。

我认为枚举的有趣风险在于无类型常量。 编写显式类型转换的人知道他们在做什么。 我愿意考虑让 Go 在某些情况下禁止显式类型转换,但认为这与枚举类型的概念完全正交。 这是一个适用于任何类型的想法。

但是无类型的常量可能会导致意外和意外地创建类型的值,这在显式类型转换中并非如此。 所以我认为@randall77explicit的建议可以简化为简单地意味着无类型常量可能不会被隐式转换为类型。 始终需要显式类型转换。

我愿意考虑在某些情况下让 Go 禁止显式类型转换的方法

@ianlancetaylor可选地禁止类型转换,无论是显式的还是隐式的,都将解决导致我首先创建此提案的问题。 只有原始包才能创建并因此满足任何类型。 这在某些方面甚至比enum解决方案更好,因为它不仅支持const声明,还支持任何类型。

@randall77 ,@ ianlancetaylor如何使explicit建议与该类型的零值一起工作?

@derekperkins完全禁止类型转换将无法在通用编码器/解码器中使用这些类型,例如encoding/*包。

@Merovius我认为@ianlancetaylor建议仅对隐式转换进行限制(例如,将无类型常量分配给受限类型)。 显式转换仍然是可能的。

@griesemer我知道 :) 但我理解@derekperkins提出不同的建议。

不允许显式转换是否会破坏我们考虑这个“显式”限定符的原因? 如果有人可以决定将任意值转换为“显式”类型,那么我们无法比现在更保证给定值是枚举常量之一。

我想它确实有助于随意或无意地使用无类型常量,这可能是最重要的事情。

我想我在质疑禁止显式转换是否符合“Go 的精神”。 禁止显式转换是朝着基于类型的编程而不是基于编写代码的编程迈出了一大步。 我认为 Go 对后者采取了明确的立场。

@griesemer @Merovius我将再次转发来自@ianlancetaylor的报价,因为这是他的建议,而不是我的建议。

我愿意考虑在某些情况下让 Go 禁止显式类型转换的方法

@rogpeppe@Merovius都提出了关于后果的好观点。 允许显式转换但不允许隐式转换并不能解决保证有效类型的问题,但失去通用编码将是一个很大的缺点。

这里有很多来回,但我认为有一些好的想法。 这是我希望看到的(或类似的)摘要,这似乎与其他人所说的一致。 我公开承认我不是语言设计师或编译器程序员,所以我不知道它的效果如何。

  1. 仅植根于基本类型的枚举(字符串、uint、int、rune 等)。 如果不需要基本类型,它可以默认为 uint?
  2. 枚举的所有有效值都必须使用类型声明——常量来声明。 无效(未在类型声明中声明)值无法转换为枚举类型。
  3. 用于调试的自动字符串表示(很高兴拥有)。
  4. 编译时检查枚举中 switch 语句的详尽性。 可选地推荐(通过go vet ?) default案例,即使对于未来的更改已经详尽(可能是错误)。
  5. 零值本质上应该是无效的(不是枚举声明中的内容)。 我个人希望它是nil ,就像切片一样。

最后一个_可能_有点争议。 而且我不确定这是否可行,但我认为它在语义上很合适——就像检查nil切片一样,可以检查nil枚举值。

至于迭代,我真的不认为我会使用它,但我看不出它有什么害处。

作为如何声明它的示例:

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

此外,推断类型和使用“点语法”的 Swift 风格将是_nice_,但绝对没有必要。

类型 EnumA int
常量 (
未知 EnumA = iota
AAA
)


类型 EnumB int
常量 (
未知 EnumB = iota
BBB
)

一个 Go 文件中不能存在两段代码,也不能存在同一个包中,甚至不能从另一个包中导入。

请只执行实现 Enum 的 C# 方式:
type Days enum {Sat, Sun, Mon, Tue, Wed, Thu, Fri}
type Days enum[int] { Sat:1 , Sun, Tue, Wed, Thu, Fri}
type Days enum[string] { Sat: "Saturay" , Sun:"Sunday" 等}

@KamyarM那比

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

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

我想请求限制对新方法/论点的评论。 很多人都订阅了这个帖子,添加噪音/重复可能会被视为不尊重他们的时间和注意力。 上面有很多讨论↑,包括对前面两个评论的详细回答。 您不会同意所说的所有内容,并且到目前为止,任何一方都不会喜欢该讨论的结果-但简单地忽略它也无助于将其推向富有成效的方向。

更好,因为它没有命名冲突问题。 还支持编译器类型检查。 您提到的方法组织它总比没有好,但编译器不会限制您可以分配给它的内容。 您可以将不是任何日期的整数分配给该类型的对象:
变种天
a =10
编译器实际上对此什么也没做。 所以这种枚举没有多大意义。 除了在 GoLand 等 IDE 中组织得更好

我想看到类似的东西

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

或使用自动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
}

这将提供简单性和易用性:

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

此外,枚举应该有内置方法来验证值,这样我们就可以从用户输入中验证某些内容:

if day in WeekDay {
  makeItWorkOn(day)
}

和简单的事情,如:

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

老实说,我最喜欢的语法是这样的(KISS):

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

@zoonman最后一个例子没有遵循 Go 的以下原则:函数声明以func开头,类型声明以type开头,变量声明以var开头, ...

@md2perpe我不想遵循 Go “类型”原则,我每天都在编写代码,我遵循的唯一一个原则 - 保持简单。
你必须编写更多的代码_以遵循原则_然后浪费更多的时间。
TBH 我是新手,但我可以批评很多事情。
例如:

struct User {
  Id uint
  Email string
}

比写和理解更容易

type User struct {
  Id uint
  Email string
}

我可以给你一个应该使用类型的例子:

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

我曾经用 Asm、C、C++、Pascal、Perl、PHP、Ruby、Python、JavaScript、TypeScript,现在用 Go 编写代码。 我看到了这一切。 这段经历告诉我,代码一定要简洁、易读易懂

我做了一些机器学习项目,需要解析 MIDI 文件。
我需要解析 SMPTE 时间码。 我发现很难使用 iota 的惯用方式,但这并不能阻止我)

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
)

当然,我可能需要对防御性编程进行一些运行时检查......

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

在某些情况下,枚举使程序员的生活更加简单。 Enums 只是一种工具,然后您正确使用它可以节省时间并提高生产力。 我认为在 Go 2 中实现这一点没有问题,就像在 c++、c# 或其他语言中一样。 这个例子只是一个玩笑,但它清楚地表明了问题。

@streeter12我看不到您的示例如何“清楚地显示问题”。 枚举如何使这段代码更好或更安全?

有一个 C# 类实现了与枚举相同的逻辑。

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

使用编译时枚举,我可以:

  1. 没有运行时检查。
  2. 显着减少团队出错的机会(你不能在编译时传递错误的值)。
  3. 它与 iota 的概念并不矛盾。
  4. 比你有一个常量名称更容易理解逻辑(重要的是你的常量代表一些低级别的协议值)。
  5. 您可以模拟 ToString() 方法来简单地表示值。 (CONNECTION_ERROR.NO_INTERNET 比 0x12 更好)。 我知道 stringer,但没有使用枚举进行显式代码生成。
  6. 在某些语言中,您可以获得值、范围等数组。
  7. 阅读代码时很容易理解(无需头脑中的计算)。

毕竟它只是防止一些常见的人为错误和节省性能的工具。

@streeter12感谢您澄清您的意思。 与 Go 常量相比,这里唯一的优点是不能引入无效值,因为类型系统除了枚举值之一之外不接受任何其他值。 这当然很好,但它也有代价:没有办法在代码之外扩展这个枚举。 外部枚举扩展是在 Go 中我们决定反对标准枚举的关键原因之一。

回答只是需要做一些不使用枚举的扩展。
FE 需要让状态机使用状态模式而不是枚举。

枚举有自己的范围。 我完成了一些没有任何枚举的大型项目。 我认为在定义代码之外扩展枚举是一个糟糕的架构决定。 您无法控制您的同事所做的事情,并且会犯一些有趣的错误)

在许多情况下,您忘记了人为因素枚举,这可以显着减少大型项目中的错误。

@streeter12不幸的是,现实情况是经常需要扩展枚举。

@griesemer扩展 enum/sum 类型会创建一个单独的且有时不兼容的类型。

即使没有明确的枚举/总和类型,这在 Go 中仍然是正确的。 如果您的包中有一个“枚举类型”,它需要 {1, 2, 3} 中的值,并且您从“扩展枚举类型”中将 4 传递给它,那么您仍然违反了隐式“类型”的合同。

如果您需要扩展枚举/求和,您还需要创建显式的 To/From 转换函数来显式处理有时不兼容的情况。

我认为这个论点与人们对这个提案或类似提案(如#19412)的脱节是我们认为权衡是“总是编写编译器可以处理的基本验证代码”而不是“有时编写你的转换函数”是很奇怪的无论如何,我可能也不得不写”。

这并不是说任何一方是对还是错,或者这是唯一需要考虑的权衡,但我想确定我注意到的双方之间沟通的瓶颈。

我认为这个论点与人们对这个提案或类似提案(如#19412)的脱节是我们认为权衡是“总是编写编译器可以处理的基本验证代码”而不是“有时编写你的转换函数”是很奇怪的无论如何,我可能也不得不写”。

说得很好

@jimmyfrasche这不是我个人描述权衡的方式。 我会说这是“总是编写编译器可以处理的基本验证代码”与“为使用 Go 的每个人都需要学习和理解的类型系统添加一个全新的概念”。

或者,让我换一种说法。 据我所知,Go 的枚举类型版本中唯一缺少的重要特性是没有验证来自无类型常量的赋值,没有检查显式转换,也没有检查所有值是否都在一个转变。 在我看来,这些特性都独立于枚举类型的概念。 我们不应该让其他语言具有枚举类型这一事实导致我们得出 Go 也需要枚举类型的结论。 是的,枚举类型会给我们那些缺失的特性。 但是真的有必要添加一种全新的类型来获得它们吗? 语言复杂性的增加值得这些好处吗?

@ianlancetaylor增加语言的复杂性当然是值得考虑的事情,“因为另一种语言拥有它”当然不是一个论点。 我个人认为枚举类型本身不值得。 (但是,它们的概括,总和类型,肯定会为我打勾)。

一个类型选择退出可分配性的一般方法会很好,尽管我不确定它在原语之外会有多大的用处。

如果没有某种方式让编译器知道合法值的完整列表,我不确定“检查在开关中处理的所有值”的概念有多普遍。 除了 enum 和 sum 类型之外,我唯一能想到的就是 Ada 的范围类型,但它们与零值自然不兼容,除非 0必须在范围内,或者生成的代码在转换或反映时处理偏移量之上。 (其他语言也有类似的类型家族,有些属于帕斯卡家族,但目前唯一想到的是 Ada)

无论如何,我特意指的是:

与 Go 常量相比,这里唯一的优点是不能引入无效值,因为类型系统除了枚举值之一之外不接受任何其他值。 这当然很好,但它也有代价:没有办法在代码之外扩展这个枚举。 外部枚举扩展是在 Go 中我们决定反对标准枚举的关键原因之一。

不幸的是,现实情况是枚举通常需要扩展。

由于我所说的原因,这个论点对我不起作用。

@jimmyfrasche 明白了; 这是一个难题。 这就是为什么在 Go 中我们没有尝试解决它,而是只提供了一种机制来轻松创建常量序列而无需重复常量值。

(延迟发送 - 是对 https://github.com/golang/go/issues/19814#issuecomment-349158748 的回应)

@griesemer确实,这绝对是对 Go 1 的正确呼吁,但其中一些值得为 Go 2 重新评估。

语言中有足够的东西可以从枚举类型中获得_几乎_所有想要的东西。 它需要比类型定义更多的代码,但生成器可以处理大部分代码,它允许您根据情况定义尽可能多或尽可能少的代码,而不是仅仅获得枚举类型附带的任何权力。

这种方法https://play.golang.org/p/7ud_3lrGfx可以为您提供一切,除了

  1. 定义包的安全性
  2. 为完整性检查开关的能力

这种方法也可以用于小而简单的求和类型†,但使用起来更尴尬,这就是为什么我认为像https://github.com/golang/go/issues/19412#issuecomment -323208336 这样的东西会添加到语言和代码生成器可以使用它来创建避免问题 1 和 2 的枚举类型。

† 请参阅https://play.golang.org/p/YFffpsvx5e以获取具有此构造的 json.Token 草图

我们认为权衡是“总是编写编译器可以处理的基本验证代码”而不是“有时编写你可能也必须编写的转换函数”,这很奇怪。

对我来说——一个激进的渐进修复支持者阵营的代表——这似乎是正确的(ish)权衡。 老实说,即使我们不是在谈论逐步修复,我也会认为这是一个更好的心理模型。

一方面,经过类型检查的枚举无论如何都只能检查插入到源代码中的值。 如果枚举通过网络传播,被持久化到磁盘或在进程之间交换,那么所有的赌注都没有了(并且大多数建议的枚举用法都属于这一类)。 因此,无论如何您都不会解决在运行时处理不兼容性的问题。 当您遇到无效的枚举值时,没有通用的一刀切默认行为。 通常你可能想出错。 有时您可能希望将其强制转换为默认值。 大多数时候你想保存它并传递它,这样它就不会在重新序列化时丢失。

当然,您可能会争辩说,仍然应该有一个信任边界,在那里检查有效性并实现所需的行为 - 并且该边界内的所有内容都应该能够信任该行为。 心理模型似乎是,这个信任边界应该是一个过程。 因为二进制文件中的所有代码都将自动更改并保持内部一致。 但是这种心理模型被逐渐修复的想法所侵蚀; 突然之间,自然信任边界变成了一个包(或者可能是存储库),作为您应用原子修复的单元和您信任的自洽单元。

而且,就个人而言,我发现这是一个非常自然和伟大的自我一致性单元。 一个包应该足够大,足以让你记住它的语义、规则和约定。 这也是为什么导出在包级别而不是类型级别工作以及为什么顶级声明在包级别而不是程序级别范围内起作用的原因。 对我来说似乎很好并且足够节省,以决定在包级别正确处理未知枚举值。 有一个未导出的函数,用于检查它并维护内部所需的行为。

我更愿意接受每个开关都需要默认情况的提议,而不是提议对枚举进行类型检查,包括穷举检查。

@Merovius正如您所说,操作系统进程和包都是信任边界。

来自流程外的信息必须在其入口处进行验证,并将其解组为流程的适当表示形式,并在失败时采取适当的措施。 那永远不会消失。 我真的没有看到任何特定于 sum/enum 类型的东西。 你可以对结构说同样的话——有时你得到额外的字段或太少的字段。 结构仍然有用。

话虽如此,使用枚举类型,您当然可以包含特定于对这些错误建模的案例。 例如

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
}

使用 sum 类型,您可以走得更远:

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
}

(前者没有那么有用,除非它保存在具有特定于错误情况的字段的结构中,但是这些字段的有效性取决于枚举的值。sum 类型负责处理这一点,因为它本质上是一个一次只能设置一个字段的结构。)

在包级别,您仍然需要处理高级验证,但低级验证随类型一起提供。 我想说减少类型的域有助于使包保持小并在您的脑海中。 它还使工具的意图更清晰,以便您的编辑器可以写出所有case X:行并让您填写实际代码,或者可以使用 linter 确保所有代码都检查所有情况(您告诉我之前编译器的详尽性)。

我真的没有看到任何特定于 sum/enum 类型的东西。 你可以对结构说同样的话——有时你得到额外的字段或太少的字段。 结构仍然有用。

如果我们谈论的是开放式枚举(比如目前由 iota 构建的枚举),那么,当然。 如果我们谈论的是封闭式枚举(人们在谈论枚举时通常会谈论这个)或具有详尽性检查的枚举,那么它们肯定是特别的。 因为它们不可扩展。

与结构的类比相当完美地解释了这一点:Go 1 兼容性承诺从任何承诺中排除了无键结构文字 - 因此,使用带键结构文字一直被认为是“最佳”的做法,因此 go vet 对其进行了检查。 原因完全一样:如果您使用的是无键结构字面量,则结构不再是可扩展的。

所以,是的。 在这方面,结构与枚举完全一样。 作为一个社区,我们已经同意最好以可扩展的方式使用它们。

话虽如此,使用枚举类型,您当然可以包含特定于对这些错误建模的案例。

您的示例仅涵盖进程边界(通过谈论网络错误),而不是包边界。 如果我在FromTheNetwork中添加“InvalidInternalState”(弥补),包的行为会如何? 我必须在它们再次编译之前修复它们的开关吗? 那么它在渐进修复模型中是不可扩展的。 他们首先需要默认情况来编译吗? 那么枚举似乎没有任何意义。

同样,拥有开放枚举是一个不同的问题。 我会在船上处理类似的事情

我想说减少类型的域有助于使包保持小并在您的脑海中。 它还使工具的意图更清晰,以便您的编辑器可以写出所有 case X: 行并让您填写实际代码,或者可以使用 linter 确保所有代码都检查所有情况

但为此,我们不需要实际的枚举作为类型。 这样的 linting 工具还可以使用iota启发式地检查const -declarations,其中每个案例都是给定的类型,并认为“一个枚举”并执行您想要的检查。 我完全同意使用这些“按惯例枚举”的工具来帮助自动完成或检查每个开关都需要具有默认值,甚至必须检查每个(已知)案例。 我什至不反对添加一个行为类似的枚举关键字; 即一个枚举是开放的(可以采用任何整数值),为您提供额外的范围,并要求在任何开关中都有一个默认值(我认为他们不会在 iota-enums 上添加足够的成本来增加成本,但至少他们不会损害我的议程)。 如果这是提议的 - 很好。 但这似乎不是该提案的大多数支持者(当然不是最初的文本)的意思。

我们可能不同意保持逐步修复和可扩展性的重要性——例如,很多人认为语义版本控制是解决它所解决问题的更好解决方案。 但是,如果您发现它们很重要,那么将枚举视为有害或无意义的做法是完全有效和合理的。 这就是我要回答的问题:人们如何合理地权衡需要在任何地方进行检查,而不是在编译器中进行检查。 回答:通过评估 API 的可扩展性和演变,这使得这些检查无论如何都必须在使用站点进行。

enum 的反对者不时说它们不可扩展,我们仍然需要在序列化/转换后进行检查,我们可以打破兼容性等。

这不是枚举问题的主要问题,而是您的开发和架构问题。
您尝试举一个使用枚举是荒谬的示例,但让我们更详细地考虑一些情况。

示例 1. 我是低级开发人员,我需要 const 一些寄存器地址,建立低级协议值等。现在在 Go 中我只有一个解决方案:是在没有 iota 的情况下使用 const,因为在很多情况下它会很难看. 我可以为一个包和按下后获得几个常量块。 我得到了所有 20 个常量,如果它们具有相同的类型和相似的名称,我可能会出错。 如果项目很大,您将收到此错误。 为了通过防御性编程防止这种情况,TDD 我们必须避免重复检查代码(重复代码 = 重复错误/测试在任何情况下)。 使用传输我们没有类似的问题,并且在这种情况下值永远不会改变(尝试找到寄存器地址在生产中发生变化的情况:))。 我们有时仍然检查我们从文件/网络等获得的值是否在范围内,但没有问题可以使这个集中化(参见 c# Enum.TryParse例如)。 在这种情况下,使用枚举可以节省开发时间和性能。

示例 2。我开发了一些带有状态/错误逻辑的小模块。 如果我将枚举设为私有,那么没有人知道这个枚举,并且您可以更改/扩展它而不会遇到 1 的所有好处。如果您的代码基于一些私有逻辑,那么您的开发就完全出错了。

示例 3. 我为广泛的应用程序开发经常更改和可扩展的模块。 使用枚举或任何其他常量来确定公共逻辑/接口是一种奇怪的解决方案。 如果您在客户端-服务器架构上添加新的枚举编号,您可能会崩溃,但使用常量您可以获得模型的不可预测状态,甚至可以将其保存到磁盘。 我更喜欢崩溃而不是不可预测的状态。 这向我们表明,向后可复制性/扩展问题是我们开发的问题而不是枚举。 如果您了解在这种情况下哪些枚举不适合,请不要使用它们。 我认为我们有足够的能力去选择。

我认为 consts 和编译时枚举之间的主要区别在于枚举有两个主要合同。

  1. 命名合同。
  2. 价值合同。
    支持和反对本段的所有论点之前都已考虑过。
    如果您使用合同编程,您很容易理解它的好处。

枚举有多少其他事物有其缺点。
Fe它不能在没有刹车兼容性的情况下改变。 但是,如果您从 SOLID 原则中了解 O,这不仅适用于枚举,也适用于一般开发。 有人会说,我用并行逻辑和可变结构使我的程序变得丑陋。 让我们禁止可变结构? 取而代之的是,我们可以添加可变/不可变结构并让开发人员选择。

说了这么多,我想指出 Iota 也有它的缺点。

  1. 它总是有 int 类型,
  2. 您需要计算头部的值。 您可能会浪费很多时间来尝试计算值,并确认没问题。
    使用 enums/const 我可以按 F12 并查看所有值。
  3. Iota 表达式是代码表达式,您也需要对此进行测试。
    在某些项目中,出于这些原因,我完全拒绝使用 iota。

您尝试举一个使用枚举很荒谬的例子

请原谅我的直言不讳,但在此评论之后,我认为您在这里没有太多理由站得住脚。

而且我什至没有按照你说的去做——也就是说,举一个例子说明使用枚举是荒谬的。 我举了一个例子来说明它们是多么必要,并说明它们是如何伤害的。

我们可以合理地不同意,但我们至少都应该真诚地争论。

示例 1

我可能会给你“注册名称”作为真正不可更改的东西,但关于协议值,我坚持让它们采用任意值以实现可扩展性和兼容性的立场是合理的。 再一次,proto2 -> proto3 包含了这种变化,而且它是根据学习经验做到的。

无论哪种方式,我都不明白为什么 linter 无法捕捉到这一点。

我得到了所有 20 个常量,如果它们具有相同的类型和相似的名称,我可能会出错。 如果项目很大,您将收到此错误。

如果您输入错误的名称,则关闭枚举对您没有帮助。 仅当您不使用符号名称而使用 int/string-literals 时。

示例 2

就个人而言,我倾向于将“单一包”牢牢地放在“不是一个大项目”的线上。 因此,我认为在扩展枚举时忘记案例或更改代码位置的可能性要小得多。

无论哪种方式,我都不明白为什么 linter 无法捕捉到这一点。

示例 3

不过,这是枚举最常见的用例。 恰当的例子:这个特定的问题使用它们作为理由。 另一个经常提到的情况是系统调用——一种伪装的客户端-服务器架构。 这个例子的概括是“两个或多个独立开发的组件交换这些值的任何代码”,它非常广泛,涵盖了它们的绝大多数用例,并且在逐步修复模型下,还包括任何导出的 API .

FTR,我仍然没有试图说服任何人枚举是有害的(我相信我不会)。 只是为了解释我是如何得出结论的,以及为什么我认为对他们有利的论点没有说服力。

它总是有 int 类型,

iota可能(不一定,但无论如何),但const块没有,它们可以有多种常量类型 - 实际上,是最常见的枚举实现的超集。

您需要计算头部的值。

同样,您不能将其用作支持枚举的论据; 您可以像在枚举声明中一样写出常量。

Iota 表达式是代码表达式,您也需要对此进行测试。

不是每个表达式都需要测试。 如果它立即显而易见,那么测试就太过分了。 如果不是,请写下常量,无论如何您都会在测试中这样做。

iota不是目前推荐的在 Go 中进行枚举的方法 - const声明是。 iota仅在写下连续或公式化的 const 声明时作为一种更通用的节省输入的方法。

是的,Go 的开放枚举显然有缺点。 上面已经广泛提到了它们:您可能会忘记开关中的一个案例,从而导致错误。 他们没有命名空间。 您可能不小心使用了最终成为无效值的非符号常量(导致错误)。
但对我来说,谈论这些缺点并根据任何提议的解决方案的缺点来衡量它们似乎更有成效,而不是采用固定的解决方案(枚举类型)并争论其具体的权衡来解决问题。

对我来说,大多数缺点都可以在当前语言中以务实的方式解决,使用 linter 工具检测某种 ​​const 声明并检查它们的用法。 命名空间不能以这种方式解决,这不是很好。 但是对于这个问题,也可能有与枚举不同的解决方案。

我可能会给你“注册名称”作为真正不可更改的东西,但关于协议值,我坚持让它们采用任意值以实现可扩展性和兼容性的立场是合理的。 再一次,proto2 -> proto3 包含了这种变化,而且它是根据学习经验做到的。

这就是为什么我说既定价值观。 Fe wav 格式基础多年来没有改变,并获得了很好的支持能力。 如果它可以保留新值,请使用枚举并添加一些值。

如果您输入错误的名称,则关闭枚举对您没有帮助。 仅当您不使用符号名称而使用 int/string-literals 时。

是的,它不能帮助我取个好名字,但它们可以帮助用一个名字组织一些价值观。 在某些情况下,它使开发过程更快。 它可以将带有自动输入的变体数量减少到一个。

不过,这是枚举最常见的用例。 恰当的例子:这个特定的问题使用它们作为理由。 另一个经常提到的情况是系统调用——一种伪装的客户端-服务器架构。 这个例子的概括是“两个或多个独立开发的组件交换这些值的任何代码”,它非常广泛,涵盖了它们的绝大多数用例,并且在逐步修复模型下,还包括任何导出的 API .

但是使用/不使用常量/枚举并不能消除问题的核心,您仍然需要考虑向后可复制性。 我想说问题不在于枚举/常量,而在于我们的用例。

就个人而言,我倾向于将“单一包”牢牢地放在“不是一个大项目”的线上。 因此,我认为在扩展枚举时忘记案例或更改代码位置的可能性要小得多。

在这种情况下,您仍然可以享受名称转换和编译时检查的好处,

不是每个表达式都需要测试。 如果它立即显而易见,那么测试就太过分了。 如果不是,请写下常量,无论如何您都会在测试中这样做。

Ofcouse 我知道不必测试所有代码行,但是如果您有先例,则必须对此进行测试或重写。 我知道如何在没有 iota 的情况下做到这一点,但我的老例子只是个玩笑。

同样,您不能将其用作支持枚举的论据; 您可以像在枚举声明中一样写出常量。

这不是枚举的论据。

@Merovius

如果我们谈论的是封闭式枚举(人们在谈论枚举时通常会谈论这个)或具有详尽性检查的枚举,那么它们肯定是特别的。 因为它们不可扩展。

它们也不能安全地扩展。

如果你有

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

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

q内使用D是安全的(除非 p 的下一个版本为 $#$ Enum(3) p添加了自己的标签),但前提是您永远不会将其传递回p :您可以获取p.Make的结果并将其状态转换为D但如果您调用p.Take您必须确保它是没有被通过q.D并且它必须确保它只获得ABC之一,或者你有一个错误。 您可以通过以下方式解决此问题

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

在语言中使用或不使用封闭类型,您都会遇到封闭类型的所有问题,但编译器不会留意您。

您的示例仅涵盖进程边界(通过谈论网络错误),而不是包边界。 如果我向 FromTheNetwork 添加“InvalidInternalState”(以弥补),包将如何表现? 我必须在它们再次编译之前修复它们的开关吗? 那么它在渐进修复模型中是不可扩展的。 他们首先需要默认情况来编译吗? 那么枚举似乎没有任何意义。

仅使用枚举类型,您仍然必须像上面那样做,并使用额外的状态定义您自己的版本并编写转换函数。

然而,sum 类型即使用作枚举也是可组合的,因此您可以以这种方式非常自然且安全地“扩展”一个。 我举了一个例子,但更明确地说,给定

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

Enum可以“扩展”

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

这一次,新版本的p添加D是完全安全的。 唯一的缺点是你必须从q.Enum内部切换到p.Enum的状态,但它是明确和清晰的,正如我提到的,你的编辑器可能会吐出骨架的开关自动退出。

但为此,我们不需要实际的枚举作为类型。 这样的 linting 工具还可以使用 iota 启发式地检查 const 声明,其中每个案例都是给定的类型,并考虑“一个枚举”并执行您想要的检查。 我完全同意使用这些“按惯例枚举”的工具来帮助自动完成或检查每个开关都需要具有默认值,甚至必须检查每个(已知)案例。

这有两个问题。

如果您有一个已定义的整数类型,其标签通过 const/iota 赋予其域的子集:

一,它可以代表一个封闭或开放的枚举。 虽然主要用于模拟封闭类型,但它也可以简单地用于为常用值命名。 考虑一个虚构文件格式的开放枚举:

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
)

这并不是说 0、1 和 42 是 Record 类型的域。 契约更加微妙,需要依赖类型来建模。 (那肯定走得太远了!)

第二,我们可以启发式地假设具有常量标签的已定义整数类型意味着域受到限制。 它会从上面得到一个误报,但没有什么是完美的。 我们可以使用 go/types 从定义中提取这个伪类型,然后遍历并找到该类型的所有切换值,并确保它们都包含必要的标签。 这可能会有所帮助,但在这一点上我们还没有表现出详尽无遗。 我们已确保覆盖所有有效值,但未证明没有创建无效值。 这样做是不可能的。 即使我们可以找到值的每个源、汇和转换,并抽象地解释它们以静态地保证没有创建无效值,我们仍然无法在运行时说出关于该值的任何信息,因为反射不知道真实的类型的域,因为它没有在类型系统中编码。

这里有一个 enum 和 sum 类型的替代方案可以解决这个问题,尽管它有自己的问题。

假设类型文字range m n创建了一个至少为 m 且最多为 n 的整数类型(对于所有 v,m ≤ v ≤ n)。 这样我们就可以限制枚举的域,比如

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

由于域的大小 = 标签的数量,因此可以 100% 置信地检查 switch 语句是否耗尽了所有可能性。 要从外部扩展该枚举,您绝对需要创建类型转换函数来处理映射,但我仍然认为您仍然需要这样做。

当然,这实际上是一个令人惊讶的微妙类型家族,无法与 Go 的其余部分配合得很好。 除了这个和一些利基用例之外,它也没有很多用途。

我们可能不同意保持逐步修复和可扩展性的重要性——例如,很多人认为语义版本控制是解决它所解决问题的更好解决方案。 但是,如果您发现它们很重要,那么将枚举视为有害或无意义的做法是完全有效和合理的。 这就是我要回答的问题:人们如何合理地权衡需要在任何地方进行检查,而不是在编译器中进行检查。 回答:通过评估 API 的可扩展性和演变,这使得这些检查无论如何都必须在使用站点进行。

对于基本的枚举类型,我同意。 在讨论开始时,如果选择它们而不是 sum 类型,我会很不高兴,但现在我明白为什么它们会有害了。 感谢您和@griesemer为我澄清这一点。

对于 sum 类型,我认为您所说的是在编译时不需要详尽的开关的正当理由。 我仍然认为封闭类型有很多好处,并且在这里检查的三种类型中,sum 类型是最灵活的,没有其他的缺点。 它们允许关闭类型而不抑制可扩展性或逐步修复,同时避免由非法值引起的错误,就像任何好的类型一样。

我在 python 和 javascript 以及其他常见的无类型语言上使用 golang 的主要原因是类型安全。 我在 Java 方面做了很多工作,而我在 golang 中错过的一件事是 Java 提供的安全枚举。

我不同意能够用枚举区分类型。 如果您需要整数,只需使用整数,就像 Java 一样。 如果您需要安全枚举,我建议您使用以下语法。

type enums enum { foo, bar, baz }

@rudolfschmidt ,我同意你的看法,它也可能看起来像这样:

type DaysOfTheWeek enum {
  Monday
  Tuesday
}

但它有一个小缺陷——当我们必须验证数据、将它们转换为 JSON 或与 FS 交互时,我们必须能够控制enum
如果我们盲目地假设枚举是一组无符号整数,我们最终会得到一个 iota。
如果我们想创新,我们必须考虑使用便利性。
例如,我如何轻松地验证传入 JSON 中的值是枚举的有效元素?
如果我告诉你软件正在改变怎么办?

假设我们有加密货币列表:

type CryptoCurrency enum {
  BTC
  ETH
  XMR
}

我们与多个第三方系统交换数据。 让我们说你有成千上万的人。
你有很长的历史,存储的数据数量。 时间过去了,比方说,比特币最终会消亡。 没有人在使用它。
因此,您决定将其从结构中删除:

type CryptoCurrency enum {
  ETH
  XMR
}

这导致数据发生变化。 因为枚举的所有值都发生了变化。 你没关系。 您可以为您的数据运行迁移。 你的伙伴呢,他们中的一些人行动不那么快,一些人没有资源,或者由于多种原因无法做到这一点。
但是你已经从他们那里摄取数据。 所以你最终会得到 2 个枚举:旧的和新的; 和使用两者的数据映射器。
这告诉我们为枚举提供定义的灵活性以及验证和编组/解组这些数据的能力。

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
}

我们必须考虑枚举和用例的适用性。

如果它可以为我们节省数千行样板代码,我们可以学习 2 个新关键字。

中间立场确实是能够验证枚举值而不限制它们的值。 枚举类型几乎保持不变——它是一堆命名常量。 枚举类型的变量可以等于枚举的基础类型的任何值。 您在此之上添加的是验证要查看的值的能力,它是否包含有效的枚举值。 并且可能还有其他好处,例如字符串化。

很多时候,我有一个协议(protobuf 或 thrift),到处都有一堆枚举。 我必须验证它们中的每一个,如果我不知道枚举值,则丢弃该消息并报告错误。 我没有其他方法可以处理这种消息。 对于枚举只是一堆常量的语言,我别无他法,只能编写大量的 switch 语句来检查所有可能的组合。 这是大量的代码,其中肯定会出现错误。 使用 C# 之类的东西,我可以使用内置支持来验证枚举,从而节省大量时间。 一些 protobuf 实现实际上是在内部这样做的,如果是这样的话,就会抛出异常。 更不用说日志记录变得多么容易 - 您可以开箱即用地进行字符串化。 protobuf 为您生成 Stringer 实现很好,但并非代码中的所有内容都是 protobuf。

但是在其他情况下,存储任何值的能力很有帮助,因为您不想丢弃消息,而是对它们做一些事情,即使它是无效的。 客户端通常可以丢弃消息,但在服务器端,您通常需要将所有内容存储在数据库中。 扔掉内容不是一种选择。

所以对我来说,验证枚举值的能力是很有价值的。 它将为我节省数千行除了验证之外什么都不做的样板代码。

将这个功能作为工具提供对我来说似乎很简单。 它的一部分已经存在于纵梁工具中。 如果有一个你会调用的工具,比如enumer Foo ,它会为Foo生成fmt.Stringer方法和(比如说)一个Known() bool方法来检查存储的值在已知值的范围内,这会缓解您的问题吗?

@Merovius在没有其他任何东西的情况下会有用。 但我通常反对autogen。 唯一有用且正常工作的情况是像 protobuf 这样的东西,你有可以编译一次的非常稳定的协议。 将它用于枚举通常感觉就像是简单类型系统的拐杖。 看看 Go 是如何与类型安全有关的,这感觉与语言本身的哲学背道而驰。 你开始在它之上开发这个基础设施,而不是语言帮助,这并不是语言生态系统的一部分。 将外部工具留给审查,而不是用于实现语言中缺少的内容。

. 将它用于枚举通常感觉就像是简单类型系统的拐杖。

因为它是 - Go 的类型系统是著名的并且有意简单化。 但这不是问题,问题是,它是否会缓解你的问题。 除了“我不喜欢它”之外,我真的不知道它是如何不喜欢的(如果你仍然假设开放枚举)。

看看 Go 是如何与类型安全有关的,这感觉与语言本身的哲学背道而驰。

Go 并不是“所有关于类型安全的”。 像 Idris 这样的语言都是关于类型安全的。 Go 是关于大规模工程问题的,因此它的设计是由它试图解决的问题驱动的。 例如,它的类型系统确实允许捕获由于 API 更改而导致的各种错误,并支持一些大规模的重构。 但它也有意保持简单,以简化学习,减少代码库的分歧并增加第三方代码的可读性。

因此,如果您感兴趣的用例(开放枚举)可以在不改变语言的情况下通过生成同样易于阅读的代码的工具来解决,那么这似乎非常符合 Go 的哲学。 特别是,添加一个作为现有功能子集的新语言功能似乎符合 Go 的设计。

因此,重申一下:如果您可以扩展使用生成您所关心的样板的工具并不能解决实际问题的方式,这将是有帮助的 - 如果没有别的,那么因为理解这一点对于通知功能的设计是必要的.

我结合了讨论中的一些想法,你怎么看?

一些基本信息:

  1. 您可以像其他所有类型一样扩展枚举。
  2. 它们像常量一样存储,但以类型名称作为前缀。 原因:当使用当前的 iota-enums 时,您可能会将枚举的名称写为每个常量的前缀。 使用此功能,您可以避免它。
  3. 它们是不可变的,并且像其他所有常量一样对待。
  4. 您可以遍历枚举。 当你这样做时,它们的行为就像一张地图。 键是枚举名称,值是枚举值。
  5. 您可以向枚举添加方法,就像对其他所有类型一样。
  6. 每个枚举值都必须自动生成方法:
  7. Name()将返回枚举变量的名称
  8. Index()将返回自动增加的枚举索引。 它从数组开始的地方开始。

代码:
```去
包主

//示例A
type Country enum[struct] { //枚举可以扩展其他类型(看例子B)
Austria("AT", "Austria", false) // 可以像 const 一样访问,但类型为
Germany("DE", "Germany", true) //前缀(例如 Country.Austria)

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

}

//枚举可以像其他类型一样拥有方法
func (c Country) test() {}

功能主要(){
println(Country.Austria.CountryName) //奥地利
println(Country.Germany.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
}

}

//示例B
类型酷枚举[int] {
非常酷(10)
酷(5)
不酷(0)
}```

@sinnlosername我认为枚举应该是很容易理解的东西。 结合前面讨论中提出的一些想法可能不一定会导致枚举的最佳想法。

我相信以下内容很容易理解:

宣言

type Day enum {
    Monday
    Tuesday
    ...
    Sunday
}

字符串转换(使用Stringer接口):

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

就是这么简单。 这样做的好处是在传递枚举时允许更强的类型安全性。

示例用法

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

如果我在这里使用string常量来表示DayIsWeekday会说任何不是"sat""sun"的字符串是工作日(即, IsWeekday("abc")会/应该返回什么?)。 相比之下,上面显示的函数的域受到限制,因此允许函数对其输入更有意义。

@ljeabmreosn

应该是

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

我放弃了等待 golang 团队以必要的方式改进语言。 我可以推荐每个人看一下 rust lang,它已经拥有所有想要的特性,比如枚举和泛型等等。

我们在 2018 年 5 月 14 日,我们仍在讨论枚举支持。 我的意思是什么鬼? 就我个人而言,我对 golang 感到失望。

我可以理解,在等待功能时可能会令人沮丧。 但是发布这样的非建设性评论并没有帮助。 请尊重您的评论。 见https://golang.org/conduct。

谢谢。

@agnivade我必须同意@rudolfschmidt。 GoLang 也绝对不是我最喜欢的语言,因为它缺乏功能、API 以及 Go 创建者对改变或接受过去错误的阻力太大。 但此刻我别无选择,因为我不是我工作场所最新项目选择哪种语言的决策者。 所以我必须克服它的所有缺点。 但老实说,这就像在 GoLang 中编写代码一样折磨;-)

我放弃了等待 golang 团队以必要的方式改进语言。

  • 必要这个词并不意味着“我想要什么”。

实际上,每种现代语言的核心特征都是必要的。 GoLang 有一些很好的特性,但如果项目保持保守,它就无法生存。 像枚举或泛型这样的特性对于不喜欢它们的人来说没有缺点,但对于想要使用它们的人来说却有很多优点。

不要告诉我“但是 go 想要保持简单”。 “简单”和“没有真正的功能”之间存在巨大差异。 Java 非常简单,但它缺少许多功能。 所以要么 Java 开发人员是巫师,要么这个论点很糟糕。

实际上,每种现代语言的核心特征都是必要的。

当然。 这些核心特征称为图灵完备性。 _Everything_ else 是一种设计选择。 图灵完备性和 C++(例如)之间有很大的空间,在那个空间中你可以找到很多语言。 该分布表明不存在全局最优值。

GoLang 有一些很好的特性,但如果项目保持保守,它就无法生存。

可能。 到目前为止,它仍在增长。 IMO,如果不是保守的话,它就不会继续增长。 我们的两种观点都是主观的,在技术上并不值钱。 规则是设计师的经验和品味。 有不同的意见很好,但这不能保证设计师会分享它。

顺便说一句,如果我想象如果人们需要的 10% 的功能被采用,那么今天的 Go 会是什么样子,我现在可能不再使用 Go。

实际上,您只是错过了我回答中最重要的论点。 也许是因为它已经与你所说的某些事情背道而驰。

“像枚举或泛型这样的特性对于不喜欢它们的人来说没有缺点,但对于想要使用它们的人来说却有很多优点。”

以及为什么你认为这种保守是 golang 成长的一个原因? 我认为这更可能与 golang 的效率和大量的标准库有关。

java 在尝试更改 java 9 中的重要内容时也经历了一种“崩溃”,这可能导致许多人寻找替代方案。 但是看看这次崩溃之前的java。 它不断增长,因为它拥有越来越多的功能,使开发人员的生活变得更轻松。

“像枚举或泛型这样的特性对于不喜欢它们的人来说没有缺点,但对于想要使用它们的人来说却有很多优点。”

这显然不是真的。 每个功能最终都会进入我想要导入的 stdlib 和/或包。 _Everyone_ 将不得不处理新功能,无论喜欢与否。

到目前为止,它仍在增长。 IMO,如果不是保守的话,它就不会还在增长

我不认为它的缓慢增长(如果有的话)是由于保守性,而是标准库,已经存在的语言特性集,工具。 这就是把我带到这里的原因。 在这方面,添加语言功能对我来说不会有任何改变。

如果我们看看 C# 和 Typescript 甚至 Rust/Swift。 他们正在疯狂地添加新功能。 C# 仍然在上下波动的顶级语言中。 Typescript 的增长速度非常快。 Rust/Swift 也一样。 另一方面,Go 在 2009 年和 2016 年大受欢迎。但在这期间,它根本没有增长,实际上正在流失。 如果新开发人员已经知道 Go 并且出于某种原因没有更早地选择它,那么 Go 就没有什么可以给新开发人员的了。 正是因为 Go 的设计停滞不前。 其他语言添加功能不是因为他们没有其他事情可做,而是因为实际用户群需要它。 人们需要新功能,以便他们的代码库在不断变化的问题领域中保持相关性。 像异步/等待。 需要解决一个实际问题。 现在您可以在多种语言中看到它,这并不奇怪。

最终会有 Go 2,你可以肯定它会带来很多新的开发者。 不是因为它是新的和闪亮的,而是因为新功能可能会说服人们最终切换或尝试它。 如果保守如此重要,我们甚至会有这些提议。

我不认为它的缓慢增长(如果有的话)是由于保守性,而是标准库,已经存在的语言特性集,工具。 这就是把我带到这里的原因。

这就是保守的结果。 如果语言每隔 [半] 年左右就会破坏一些东西/一切,那么你对 Go 的评价就不会是什么,因为给你带来这种东西的人会少得多。

在这方面,添加语言功能对我来说不会有任何改变。

您确定吗? 往上看。


顺便说一句,你看过2017 年的调查结果吗?

如果语言每隔 [半] 年左右就会破坏一些东西/一切

然后不要破坏任何东西。 C# 添加了大量特性,并且从未违反向后兼容性。 这也不是他们的选择。 我认为 C++ 也是如此。 如果 Go 不能在不破坏某些东西的情况下添加功能,那么这是 Go 的问题,并且可能是它的实现方式。

顺便说一句,您看过 2017 年的调查结果吗?

我的评论基于 2017/2018 年的调查、TIOBE 指数以及我对各种语言情况的一般观察。

@cznic
每个人都必须处理它们,但您不需要使用它们。 如果您更喜欢使用 iota 枚举和映射来编写代码,那么您仍然可以这样做。 如果你不喜欢泛型,那就使用没有它们的库。 Java 证明了两者都有可能。

然后不要破坏任何东西。

好主意。 然而,许多(如果不是大多数)对语言提出的更改,_包括这个_,都是重大更改。

C# 添加了大量特性,并且从未违反向后兼容性。

请检查您的事实: Visual C# 2010 Breaking Changes 。 (第一个网络搜索结果,我只能猜测它是否是唯一的例子。)

我的评论基于 2017/2018 年的调查、TIOBE 指数以及我对各种语言情况的一般观察。

那么,当调查结果显示受访者数量每年增长 70% 时,您怎么能看到语言没有增长呢?

您如何定义“重大变化”? 添加枚举或泛型后,每一行 go 代码仍然可以工作。

每个人都必须处理它们,但您不需要使用它们。 如果您更喜欢使用 iota 枚举和映射来编写代码,那么您仍然可以这样做。 如果你不喜欢泛型,那就使用没有它们的库。 Java 证明了两者都有可能。

我不能同意。 一旦语言得到了,例如泛型,那么即使我可能不会使用它们,它们也会被到处使用。 即使在内部不更改 API 时也是如此。 结果是我深受它们的影响,因为肯定没有办法在不减慢使用它们的任何程序的构建速度的情况下将泛型添加到语言中。 又名“没有免费的午餐”。

您如何定义“重大变化”? 添加枚举或泛型后,每一行 go 代码仍然可以工作。

当然不是。 此代码将不再与此提案一起编译:

package foo

var enum = 42

必要这个词并不意味着“我想要什么”。

当然,这并不意味着,我从来没有这个意思。 当然,您可以回答这些功能不是必需的,但我可以回答一般来说什么是必要的。 没有什么是必要的,我们可以回到笔和纸上。

Golang 声称是大型团队的语言。 我不确定您是否可以使用 golang 开发大型代码库。 为此,您需要静态编译和类型检查,以尽可能避免运行时错误。 如果没有枚举和泛型,你怎么能这样做? 这些功能甚至都不是花哨或美好的,但对于认真的开发来说绝对必不可少。 如果您没有它们,您最终会在任何地方使用接口{}。 如果您被迫在代码中使用接口{},那么拥有数据类型有什么意义?

当然,如果你没有选择,你也会这样做,但是如果你有像 rust 这样的替代品已经提供了所有这些东西,而且执行速度比 golang 还快,那你为什么还要选择呢? 我真的想知道 Go 是否有这样的心态的未来:

必要这个词并不意味着“我想要什么”。

我尊重对开源的所有贡献,如果 golang 是一个爱好项目,那它很好,但是 golang 想要被认真对待,目前它对一些无聊的开发人员来说更像是一个玩具,我看不到改变它的意愿。

api 不需要更改,只有新的 api 部分可能使用泛型,但互联网上可能总是有没有泛型的替代方案。

而且,编译速度稍慢一点,并且称为“枚举”的变量影响很小。 实际上 99% 的人甚至都不会注意到它,而另外 1% 的人只需要添加一些可以容忍的小改动。 这无法与例如 java 的拼图相提并论。

而且,编译速度稍慢一点,并且称为“枚举”的变量影响很小。 实际上 99% 的人甚至都不会注意到它,而另外 1% 的人只需要添加一些可以容忍的小改动。

如果有人能够提供具有如此出色性能的设计和实现,每个人都会很高兴。 请为#15292 做出贡献。

但是,如果这是一个名为“在没有任何支持数据的情况下拉取任何对我有利的数字”的游戏,那就抱歉了,但我不参与。

您对与仿制药的速度差异有任何数字吗?

是的,这些数字没有任何数据支持,因为它们只能说明存在称为“枚举”的变量的概率不是很高。

我想提醒大家,有很多人订阅了这个问题,具体问题是枚举是否以及如何添加到 Go 中。 “Go 是一门好语言吗?”的一般性问题和“应该更专注于交付功能吗?” 可能更好地在不同的论坛中讨论。

您对与仿制药的速度差异有任何数字吗?

不,这就是为什么我没有发布任何内容。 我只发布了,成本不能为零。

是的,这些数字没有任何数据支持,因为它们只能说明存在称为“枚举”的变量的概率不是很高。

那是混在一起的。 放缓是关于仿制药。 “枚举”是关于向后兼容性,而您的错误“_Every_ 行代码在添加枚举或泛型后仍然可以工作。” 宣称。 (强调我的)

@Merovius你说得对,我现在闭嘴了。

回到枚举类型,这就是这个问题的意义所在,我完全理解为什么 Go 需要泛型的论点,但我对为什么 Go 需要枚举类型的论点更加不稳定。 事实上,我在https://github.com/golang/go/issues/19814#issuecomment -290878151 中问过这个问题,但我仍然对此感到不安。 如果有一个好的答案,我错过了。 有人可以重复或指出吗? 谢谢。

@ianlancetaylor我认为用例并不复杂,需要一种类型安全的方法来确保值属于预定义的值集,这在 Go 中是不可能的。 唯一的解决方法是在代码的每个可能入口点手动验证,包括 RPC 和函数调用,这本质上是不可靠的。 迭代的其他语法细节使许多常见用例更容易。 无论您是否认为有价值是主观的,显然上述论点都没有说服力,所以我基本上已经放弃了在语言层面上解决这个问题。

@ianlancetaylor :一切都与类型安全有关。 您使用类型来最小化由于拼写错误或使用不兼容的类型而导致运行时错误的风险。 目前你可以写进去

if enumReference == 1

因为目前枚举只是数字或其他原始数据类型。

该代码根本不应该出现,应该避免。 几年前您在 Java 社区中进行过同样的讨论,这就是他们引入枚举的原因,因为他们了解枚举的重要性。

你应该只能写

if enumReference == enumType

你不需要太多的幻想来想象在哪些场景中if enumReference == 1可以以更隐蔽的方式发生并导致你只会在运行时看到的其他问题。

我只想提一下:Go 有它的潜力,但奇怪的是,多年来已经证明和理解的事物和概念在这里讨论,就像你讨论编程的新概念或范式一样。 如果您有另一种确保类型安全的方法,也许有比枚举更好的方法,但我看不到。

Go 有它的潜力,但奇怪的是,多年来已经证明和理解的事物和概念在这里讨论,就像你讨论编程的新概念或范式一样。

Afais,尤其是在关注关于泛型、总和类型等的其他讨论时......与其说是否拥有它,不如说是如何实现它。 Java 类型系统具有极强的可扩展性和规范性。 这是一个巨大的差异。

在 Go 中,人们试图想出一些方法来为语言添加特性,同时不增加编译器的复杂性。 这通常不能很好地工作,并使他们放弃那些最初的想法。

虽然我也认为这些优先事项在目前的形式和质量上是相当荒谬的,但最好的办法是提出尽可能简单且破坏性最小的实施方案。 其他任何事情都不会让你走得更远,imo。

@derekperkins @rudolfschmidt谢谢。 我想明确一点,尽管 C++ 具有枚举类型,但您建议的功能不在 C++ 中。 所以这没有什么明显的。

一般来说,如果枚举类型的变量只能接受该枚举的值,那将是无用的。 特别是必须有从任意整数到枚举类型的转换; 否则你不能通过网络连接发送枚举。 好吧,你可以,但是你必须为每个枚举值编写一个带有 case 的 switch,这看起来真的很乏味。 那么在进行转换时,编译器是否会在类型转换期间检查该值是否为有效的枚举值? 如果值无效,它会恐慌吗?

枚举值是否需要是连续的,或者它们可以像 C++ 中那样采用任何值?

在 Go 中,常量是无类型的,所以如果我们允许从整数到枚举类型的转换,那么禁止if enumVal == 1会很奇怪。 但我想我们可以。

Go 的一般设计原则之一是编写 Go 的人编写代码,而不是类型。 我还没有看到枚举类型有什么优势可以帮助我们编写代码。 它们似乎添加了一组我们在 Go 中通常没有的类型约束。 无论好坏,Go 都没有提供控制类型值的机制。 所以我不得不说,对我来说,支持在 Go 中添加枚举的论点似乎还没有说服力。

我会重复自己,但我赞成保持枚举的现状并在它们之上添加功能:

  • enum 类型有一个基础值类型和几个与之关联的命名常量
  • 只要它们的基础类型兼容,编译器就应该允许从任意值转换为枚举值。 int类型的任何值都应该可以转换为任何整数枚举类型。
  • 允许导致无效枚举值的转换。 枚举类型不应该对变量可以采用的值施加任何限制。

它提供的最重要的是:

  • 枚举值的字符串化。 根据我的经验,对 UI 和日志记录非常有用。 如果枚举值有效,则字符串化返回常量的名称。 如果它无效,则返回基础值的字符串表示形式。 如果-1不是某个枚举类型Foo的有效枚举值,则字符串化应该只返回-1
  • 允许开发人员确定该值是否在运行时是有效的枚举值。 在使用任何类型的协议时非常有用。 随着协议的发展,可以引入程序不知道的新枚举值。 或者这可能是一个简单的错误。 现在,您要么必须确保枚举值是严格按顺序排列的(不是您可以始终强制执行的),要么手动检查每个可能的值。 那种代码变得非常大非常快,而且肯定会发生错误。
  • 可能允许开发人员枚举枚举类型的所有可能值。 我看到人们在这里要求这个,其他语言也有这个,但我实际上不记得自己曾经需要这个,所以我没有个人经验支持这个。

我的理由是关于编写代码和避免错误。 对于开发人员来说,所有这些任务都是繁琐且不必要的,甚至不需要引入外部工具来使代码和构建脚本变得复杂。 这些功能涵盖了我需要的所有枚举,而不会过度复杂化和限制它们。 我认为 Go 不需要 Swift 甚至 Java 中的枚举之类的东西。


有关于在编译时验证 switch 语句涵盖每个可能的枚举值的讨论。 有了我的提议,这将毫无用处。 进行耗尽检查不会覆盖无效的枚举值,因此您仍然必须使用默认情况来处理这些值。 这是支持逐步代码修复所必需的。 我认为,我们在这里唯一能做的就是在 switch 语句没有默认情况下发出警告。 但是即使不改变语言也可以做到这一点。

@ianlancetaylor我认为你的论点有一些缺陷。

一般来说,如果枚举类型的变量只能接受该枚举的值,那将是无用的。 特别是必须有从任意整数到枚举类型的转换; 否则你不能通过网络连接发送枚举。

程序员的抽象很好; Go提供了许多抽象。 例如,以下代码无法编译:

package main

import "fmt"

const NULL = 0x0

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

但是在C中,这种风格的程序可以编译。 这是因为Go是强类型的,而C不是。

枚举的索引可能在内部存储,但作为抽象对用户隐藏,类似于变量的地址。

@zerkms是的,这是一种可能性,但鉴于d的类型,类型推断应该是可能的; 但是,枚举的合格使用(如您的示例中)更容易阅读。

@ianlancetaylor这是您正在谈论的枚举的 C 版本。 我敢肯定很多人会喜欢这样,但是,imo:

枚举值不应具有任何数字属性。 每个枚举类型的值应该是它们自己的离散标签的有限宇宙,在编译时强制执行,与任何数字或其他枚举类型无关。 您可以对一对这样的值做的唯一事情是==!= 。 其他操作可以定义为方法或函数。

该实现会将这些值编译为整数,但这不是一个基本的事情,有任何正当理由直接暴露给程序员,除非通过不安全或反射。 出于同样的原因,你不能做bool(0)来获得false

如果要将枚举转换为数字或任何其他类型,请写出所有情况并包括适合情况的错误处理。 如果这很乏味,您可以使用像 stringer 这样的代码生成器,或者至少使用一些东西来填写 switch 语句中的情况。

如果您将值发送到进程外,如果您遵循定义明确的标准,或者您知道您正在与从源代码编译的程序的另一个实例交谈,或者您需要做一些事情,那么 int 是很好的即使这可能会导致问题,也可能适合最小的空间,但通常这些都不成立,最好使用字符串表示,这样该值就不受类型定义的源顺序的影响。 您不希望进程 A 的 Green 成为进程 B 的 Blue,因为其他人决定应在 Green 之前添加 Blue 以保持定义中的字母顺序:您想要unrecognized color "Blue"

这是抽象地表示多个状态的一种很好、安全的方法。 它让程序定义这些状态的含义。

(当然,您通常希望将数据与这些状态相关联,并且该数据的类型因状态而异......)

@ljeabmreosn我的观点是,如果 Go 允许从整数转换为枚举类型,那么无类型常量自动转换为枚举类型是很自然的。 您的反例不同,因为 Go 不允许从整数转换为指针类型。

@jimmyfrasche如果您必须编写一个在整数和枚举类型之间进行转换的开关,那么我同意这在 Go 中可以干净地工作,但坦率地说,它本身似乎没有足够的用处。 它成为 sum 类型的特例,参见#19412。

这里有很多建议。

一般性评论:对于任何不公开可以转换为枚举的基础值(例如 int)的提案,这里有一些问题需要回答。

枚举类型的零值是多少?

你如何从一个枚举到另一个枚举? 我怀疑对于许多人来说,一周中的几天是枚举的典型示例,但人们可能会合理地希望从周三到周四“递增”。 我不想为此写一个大的 switch 语句。

(此外,关于“字符串化”,一周中某一天的正确字符串取决于语言和区域设置。)

@josharian字符串化通常意味着由编译器自动将枚举值的名称转换为字符串。 没有本地化或任何东西。 如果你想在此基础上构建一些东西,比如本地化,那么你可以通过其他方式来实现,而其他语言提供了丰富的语言和框架工具来做到这一点。

例如,某些 C# 类型具有ToString覆盖,它也采用文化信息。 或者您可以使用DateTime对象本身并使用它的ToString方法来接受格式和文化信息。 但是这些覆盖不是标准的,每个人都继承的object类只有ToString() 。 很像 Go 中的 stringer 接口。

所以我认为本地化应该在这个提议和一般枚举之外。 如果你想实现它,那就用其他方式来实现。 例如,自定义纵梁界面。

@josharian因为,在实现方面,它仍然是一个 int 并且零值是所有位为零,零值将是源顺序中的第一个值。 这有点泄露了完整性,但实际上非常好,因为您可以选择零值,例如决定一周是从星期一还是星期日开始。 当然,如果您更改第一个元素,其余术语的顺序没有这样的影响并且重新排序值可能会产生非微不足道的影响,这不太好。 不过,这与 const/iota 并没有什么不同。

重新字符串化@creker所说的。 不过,要扩展,我希望

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

打印星期日不是 0。标签是值,而不是它的表示。

需要明确的是,我并不是说它应该有一个隐式 String 方法——只是标签作为类型的一部分存储并且可以通过反射访问。 (也许 Println 从一个枚举或类似的东西中调用一个 reflect.Value 上的 Label()?还没有深入研究 fmt 是如何做它的巫术的。)

你如何从一个枚举到另一个枚举? 我怀疑对于许多人来说,一周中的几天是枚举的典型示例,但人们可能会合理地希望从周三到周四“递增”。 我不想为此写一个大的 switch 语句。

我认为反思或大开关是正确的。 通用模式可以很容易地用 go generate 填充,以在该类型或该类型的工厂函数上创建方法(甚至可能被编译器识别以将其降低为表示的算术)。

假设所有枚举都有一个总顺序或者它们是循环的,这对我来说是没有意义的。 给定type failure enum { none; input; file; network } ,强制无效输入小于文件故障或增加文件故障导致网络故障或增加网络故障导致成功真的有意义吗?

假设主要用途是循环有序值,另一种处理方法是创建一个新的参数化整数类型类。 这是不好的语法,但是,为了讨论,假设它是I%N ,其中I是整数类型,而N是整数常量。 所有具有这种类型值的算术都是隐式 mod N。然后你可以做

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

所以星期六 + 1 == 星期日和工作日(456)== 星期一。 不可能构造一个无效的工作日。 不过,它在 const/iota 之外可能很有用。

因为当你根本不希望它是数字时,正如@ianlancetaylor指出的那样,我真正想要的是 sum 类型。

引入任意模算术类型是一个有趣的建议。 然后枚举可能是这种形式,它为您提供了一个简单的 String 方法:

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

type Weekday = uint % len(Weekdays)

结合任意大小的整数,这也可以获得 int128、int256 等。

您还可以定义一些内置函数:

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

编译器可以证明比以前更多的界限。 API 可以通过类型提供更精确的断言,例如 math/bits 中的函数Len64 math/bits可以返回uint % 64

在 RISC-V 端口上工作时,我想要一个uint12类型,因为我的指令编码组件是 12 位的; 那可能是uint % (1<<12) 。 许多位操作,尤其是协议,都可以从中受益。

当然,缺点是显着的。 Go 倾向于代码而不是类型,这是类型繁重的。 像 + 和 - 这样的操作会突然变得像 % 一样昂贵。 如果没有某种类型的参数,您可能必须转换为规范的uint8uint16等,以便与几乎任何库函数互操作,并且转换回来可以隐藏边界失败(除非我们有办法进行panic-on-out-of-range转换,这会引入其自身的复杂性)。 而且我可以看到它被过度使用,例如将uint % 1000用于 HTTP 状态代码。

不过,这是一个有趣的想法。 :)


其他小回复:

这有点泄露了内在

这让我觉得他们真的是整数。 :)

常见的模式可以很容易地用 go generate 填充

如果无论如何您都必须使用枚举生成代码,那么在我看来,您还可以生成字符串函数和边界检查等,并通过代码生成而不是语言支持的权重来进行枚举。

假设所有枚举都有一个总顺序或者它们是循环的,这对我来说是没有意义的。

很公平。 这让我认为拥有一些具体的用例将有助于明确我们想要从枚举中得到什么。 我有点怀疑不会有一套明确的要求,并且使用其他语言结构(即现状)来模拟枚举最终将是最有意义的。 但这只是一个假设。

重新字符串化@creker所说的。

很公平。 但我确实想知道有多少案例最终会像一周中的几天一样。 当然,任何面向用户的东西。 字符串化似乎是对枚举的主要要求之一。

不过,真正整数的@josharian枚举可能需要类似的机制。 否则,什么是enum { A; B; C}(42)

您可以说这是一个编译器错误,但这在更复杂的代码中不起作用,因为您可以在运行时与整数相互转换。

它是 A 或运行时恐慌。 在任何一种情况下,您都将添加一个具有有限域的整数类型。 如果它是运行时恐慌,则您正在添加一个整数类型,当其他类型环绕时,它会在溢出时发生恐慌。 如果是 A,则您已通过某种仪式添加了 uint%N。

另一种选择是让它不是 A、B 或 C,但这就是我们今天使用 const/iota 所拥有的,因此没有任何收益。

您说 int%N 不会使其成为语言的所有原因似乎同样适用于有点 int 的枚举。 (尽管如果包括类似的东西,我绝不会生气)。

消除内在性消除了这个难题。 当想要添加一些完整性时,它需要代码生成,但它也让您可以选择不这样做,这使您可以控制要引入的完整性和类型:您可以添加 no next" 方法,循环的 next 方法,或者如果你从边缘掉下来返回错误的 next 方法。 (你也不会得到像Monday*Sunday - Thursday这样的东西是合法的)。 额外的刚性使其成为更具延展性的建筑材料。 有区别的联合很好地模拟了非整数类型: pick { A, B, C struct{} }等等。

在语言中包含此类信息的主要好处是

  1. 非法值是非法的。
  2. 该信息可用于反映和 go/types 允许程序对其进行操作而无需做出假设或注释(目前无法反映)。

在语言中包含此类信息的主要好处是: 非法值是非法的。

我认为重要的是要强调不是每个人都认为这是一种好处。 我当然不会。 它通常使消费值更容易,它通常使生产它们更难。 到目前为止,您似乎更重,这取决于个人喜好。 因此,它是否是一个整体的净收益的问题也是如此。

我也没有看到禁止非法值的意义。 如果您已经有办法自己检查有效性(如我上面的建议),那么这个限制有什么好处? 对我来说,这只会使事情复杂化。 在我的应用程序中,大多数情况下的枚举可能包含无效/未知值,您必须根据应用程序解决这个问题 - 完全丢弃,降级到某些默认值或按原样保存。

我想在您的应用程序与外部世界隔离并且无法接收无效输入的非常有限的情况下,不允许无效值的严格枚举可能很有用。 就像只有您可以看到和使用的内部枚举一样。

带有 iota 的 const 在编译时不安全,检查会延迟到运行时,并且安全检查不在类型级别。 所以我认为 iota 不能从字面上取代 enum,我更喜欢 enum 因为它更强大。

非法值是非法的。
我认为重要的是要强调不是每个人都认为这是一种好处。

我不明白这个逻辑。 类型是一组值。 您不能将类型分配给其值不在该类型中的变量。 我是不是误会了什么?

PS:我同意枚举是 sum 类型的一种特殊情况,这个问题应该优先于这个问题。

让我换个说法/更准确地说:并不是每个人都认为关闭枚举是一个好处。

如果您想以这种方式严格,那么a)“非法值是非法的”是重言式,b)因此不能算作好处。 对于基于 const 的枚举,在您的解释中,非法值也是非法的。 该类型只允许更多值。

如果枚举是整数并且任何整数都是合法的(从类型系统的角度来看),那么唯一的好处是该类型的命名值在反射中。

这基本上只是 const/iota,但您不必运行 stringer,因为 fmt 包可以使用反射获取名称。 (如果您希望字符串与源中的名称不同,您仍然必须运行 stringer)。

@jimmyfrasche字符串化只是一个不错的奖励。 正如您在上面的提案中所读到的,对我而言,主要功能是能够在运行时检查给定值是否是给定枚举类型的有效值。

例如,给定这样的事情

type Foo enum {
    Val1 = 1
    Val2 = 2
}

和反射方法一样

func IsValidEnum(v {}interface) bool

我们可以做这样的事情

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

对于一个真实世界的示例,您可以查看 C# 中的枚举,在我看来,它完美地抓住了这个中间立场,而不是盲目地遵循 Java 所做的。 要检查 C# 中的有效性,请使用Enum.IsDefined静态方法

@crecker那和 const/iota 之间的唯一区别是
存储在反射中的信息。 这对整体来说并没有太大的收获
新型类型。

有点疯狂的想法:

存储在同一个包中声明的所有 const 的名称和值
作为他们定义的类型,以一种反射可以得到的方式。 这将是
不过,将那种狭窄的 const 用法单独列出来是很奇怪的。

对我来说的主要功能,你可以在我上面的建议中读到

IMO 这说明了拖累这个讨论的主要事情之一:缺乏明确的“主要特征”集是什么。 对此,每个人的想法似乎都略有不同。
就个人而言,我仍然喜欢体验报告的格式来发现那套。 列表中甚至有一个(尽管就我个人而言,我仍然会评论“出了什么问题”部分只提到了可能出错的地方,而不是实际出错的地方)。 也许添加一对,说明缺乏类型检查导致中断/错误或例如无法进行大规模重构的地方,会有所帮助。

@jimmyfrasche但这解决了许多应用程序中的一个大问题——验证输入数据。 如果没有类型系统的任何帮助,您必须手动完成,而这不是您可以在几行代码中完成的。 拥有某种形式的类型辅助验证可以解决这个问题。 在此之上添加字符串化将简化日志记录,因为您将拥有正确格式化的名称而不是基础类型值。

另一方面,使枚举严格会严重限制可能的用例。 例如,现在您不能轻松地在协议中使用它们。 为了保留无效值,您必须删除枚举并使用纯值类型,如果需要,可能稍后将它们转换为枚举。 在某些情况下,您可以删除无效值并引发错误。 在其他情况下,您可以降级到某个默认值。 无论哪种方式,您都在与类型系统的限制作斗争,而不是帮助您避免错误。

只需查看 Java 的 protobuf 必须生成什么才能处理 Java 枚举。

@Merovius关于验证,我想我已经多次讨论过了。 我不知道除了可以添加什么 - 如果没有验证,您必须编写大量的复制粘贴代码来验证您的输入。 问题很明显,以及提议的解决方案如何帮助解决这个问题。 我不从事一些每个人都知道的大型应用程序,但是该验证代码中的错误使我在多种语言中使用了相同的枚举概念,我希望看到对此进行一些处理。

另一方面,我没有看到(如果我错过了什么,请道歉)任何支持实现不允许无效值的枚举的论点。 理论上它很好而且很整洁,但我只是看不到它在实际应用中对我有帮助。

人们想要从枚举中获得的功能并不多。 就无效值而言,字符串化、验证、严格/松散、枚举——这几乎就是我所看到的。 每个人(当​​然包括我)都只是在这一点上随机播放它们。 严格/松散似乎是争论的焦点,因为它们具有冲突的性质。 我不认为每个人都会同意一个或另一个。 也许解决方案可能是以某种方式合并它们并让程序员选择,但我不知道有任何语言可以看到它在现实世界中如何工作。

@crecker我建议将常量存储在上面的导出数据中
情况将允许您要求的那种东西
没有引入一种新的类型。

我不确定这是惯用的方式,而且我对该语言也很陌生,但是以下内容很简洁

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

优点

缺点

我们真的需要枚举吗?

是什么阻止某人做这样的事情:

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

这样一个变量的消费者可能会或可能不会通过适当检查预期值来捕捉到这一点(假设 switch 语句中的假设没有糟糕的下降,例如,不是星期六或星期日的任何事情都是工作日),但它不会是直到运行时。 我认为人们更愿意在编译时而不是运行时捕获这种类型的错误。

@bpkroth
通过在自己的包中包含Day并仅公开选定的字段和方法,我无法在package day Day类型的新值
另外,这样我不能将匿名结构传递给getTask

./day/day.go

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

我一生中从未见过任何其他语言坚持不添加最简单和有用的功能,如枚举、三元运算符、使用未使用的变量进行编译、求和类型、泛型、默认参数等......

Golang 是一个社会实验,看看开发者有多愚蠢吗?

@gh67uyyghj有人将您的评论标记为离题! 我想有人会对我的回复做同样的事情。 但我想你的问题的答案是肯定的。 在 GoLang 中,无特征意味着有特征,所以 GoLang 没有的任何东西实际上都是 GoLang 具有的其他编程语言没有的特性!

@L-oris 这是用类型实现枚举的一种非常有趣的方式。 但这感觉很尴尬,并且使用 enum 关键字(这必然会使语言更加复杂)会使以下操作变得更容易:

  • 原因

在您的示例中(这很好,因为它在今天有效)具有枚举(以某种形式)意味着需要:

  • 创建结构类型
  • 创建一个方法
  • 创建变量(甚至不是常量,尽管库的用户不能更改这些值)

这需要更长的时间(虽然不是_那_更长)来读取、写入和推理(辨别它代表并应该用作枚举)。

因此,我认为语法建议在简单性和语言附加值方面是正确的。

谢谢@andradei
是的,这是一种解决方法,但我觉得该语言的目的是保持小而简单
我们也可以争辩说我们错过了课程,但是让我们转向 Java :)

我宁愿专注于 Go 2 提案,更好的错误处理,例如。 将为我提供比这些枚举更多的价值

回到你的观点:

  • 它没有那么多样板; 在最坏的情况下,我们可以有一些生成器(但是,真的有那么多代码吗?)
  • 通过添加一个新关键字,我们实现了多少“简单性”,以及它可能具有的整个特定行为集?
  • 对方法有点创意也可以为这些枚举添加有趣的功能
  • 为了可读性,更多的是习惯它; 也许在它上面添加评论,或者为你的变量添加前缀
package day

// Day Enum
type Day struct {
    value string
}

@L-oris 我明白了。 我也对 Go 2 提案感到兴奋。 我认为泛型会比枚举更多地增加语言的复杂性。 但要坚持你的观点:

  • 确实没有那么多样板
  • 我们必须检查枚举的概念有多广为人知,我想说大多数人都知道它是什么(但我无法证明)。 语言的复杂性将以一个很好的“代价”来支付它的好处。
  • 没错,没有枚举是我仅在检查generetad protobuf 代码以及尝试创建模拟枚举的数据库模型时才出现的问题。
  • 这也是真的。

我一直在思考这个提议,我可以看到简单性对生产力的巨大价值,以及为什么你倾向于保留它,除非显然有必要进行更改。 枚举也可以彻底改变语言,它不再是 Go,评估其优缺点似乎需要很长时间。 所以我一直认为像你这样的简单解决方案,其中代码仍然易于阅读,至少目前是一个很好的解决方案。

伙计们,将来真的很想要这个功能! _nowadays_ 中的指针和定义“枚举”的方式不太融洽。 例如:https: //play.golang.org/p/A7rjgAMjfCx

我对枚举的提议如下。 我们应该将其视为一种新类型。 例如,我想使用具有任意结构和以下实现的枚举类型:

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

这个结构怎么编组,怎么工作,怎么改成字符串等等,都是可以理解的。
当然,如果我可以覆盖“下一步”功能会很棒。

为此,Go 必须首先支持深层不可变结构。 如果没有不可变的类型,我可以想象你可以用枚举来做到这一点来拥有同样的东西:

type Status enum {
  Started
  Stopped
}

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

我认为它应该看起来更简单

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

Go2的提案

逻辑枚举应该提供类型接口。
我说过前面的枚举应该是分开的。
它被显式命名为绑定到特定命名空间的常量。

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

枚举是类型扩展,“常量容器”。

类型爱好者

那些想要将其视为类型的人的语法替代方案

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

但是我们也可以避免那些显式的顶级声明

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

验证示例保持不变。

但万一

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
}

Status1.Started == Status2.Started 怎么样?
关于编组?

如果我换个位置?

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

我同意@Goodwine关于不可变类型的观点。

编组是一个有趣的问题。
这一切都取决于我们将如何对待潜在价值。 如果我们要使用实际值,则Status1.Started将等于Status2.Started
如果我们使用符号解释,这些将被视为不同的值。

插入一些东西会改变值(与iota完全相同)。
为了避免这种情况,开发人员必须在声明的同时指定值。

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

这是显而易见的事情。
如果我们想避免此类问题,我们必须根据枚举值的词法解释提供可预测的编译器输出。 我假设最简单的方法 - 除非定义了自定义类型转换,否则构建哈希表或坚持符号名称(字符串)。

我喜欢 Rust 是如何实现枚举的。

未指定类型的默认值

enum IpAddr {
    V4,
    V6,
}

自定义类型

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

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

复杂类型

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

当然,即使有像 C# 这样存储为整数类型的简单枚举也会很棒。

以上超越enum s,它们是 _discriminated unions_,它们确实更强大,尤其是 _pattern matching_,它可能是switch的一个小扩展,例如:

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

我不需要对枚举进行任何编译时检查,因为这可能是危险的,如前所述

我需要多次迭代给定类型的所有常量:

  • 要么用于验证(如果我们非常确定我们只想接受这个或简单地忽略未知选项)

    • 或可能的常量列表(想想下拉菜单)。

我们可以使用 iota 进行验证并指定列表的末尾。 然而,除了在代码内部使用 iota 之外的任何其他东西,将是相当危险的,因为在错误的行插入一个常量会破坏东西(我知道我们需要知道我们在编程中把东西放在哪里,但是像这样的错误是比其他东西更难找到)。 此外,当它是一个数字时,我们没有描述常量实际代表什么。 这就引出了下一点:

一个不错的附加功能是为其指定字符串化名称。

是什么阻止某人做这样的事情:

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

这样一个变量的消费者可能会或可能不会通过适当检查预期值来捕捉到这一点(假设 switch 语句中的假设没有糟糕的下降,例如,不是星期六或星期日的任何事情都是工作日),但它不会是直到运行时。 我认为人们更愿意在编译时而不是运行时捕获这种类型的错误。

@L-oris 那么这个呢:

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
}

我们想要的不是 [return "nothing to do"] 导致的运行时沉默和奇怪的 BUG,而是编译时/编码时错误报告
理解?

  1. enum确实是新类型,这就是type State string所做的,没有惯用的需要引入新的关键字。 Go 不是为了在源代码中节省空间,而是关于可读性和目的明确性。

  2. 缺乏类型安全,混淆了新的基于string - 或int的实际字符串/整数类型是关键障碍。 所有枚举子句都声明为const ,它创建了一组编译器可以检查的已知值。

  3. Stringer接口是将任何类型表示为人类可读文本的惯用语。 如果没有自定义, type ContextKey string枚举这是字符串值,而对于iota生成的枚举,它是整数,很像 JavaScript 中的XHR ReadyState 代码(0 - 未发送,4 - 完成)。

    相反,问题在于自定义func (k ContextKey) String() string实现的易错性,这通常使用必须包含每个已知枚举子句常量的开关来完成。

  4. 在像 Swift 这样的语言中,有一个“穷举开关”的概念。 对于针对一组const的类型检查和构建调用该检查的惯用方式来说,这是一个很好的方法。 String()函数作为一种常见的必需品,是一个很好的实现案例。

提议

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 Swift 枚举中的关联值是我最喜欢的噱头之一。 在围棋中没有他们的位置。 如果你想在你的枚举数据旁边有一个值——使用一个强类型的struct包装这两者。

几个月前,我为检查枚举类型是否正确处理的 linter 编写了一个概念验证。 https://github.com/loov/enumcheck

目前它使用注释将事物标记为枚举:

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

我一直在弄清楚如何处理所有隐式转换,但它在基本情况下工作得很好。

请注意,目前它仍在进行中,因此情况可能会发生变化。 例如,它可以使用一些存根包来注释类型,而不是注释,但目前注释已经足够好了。

Go1 中当前的枚举实现是我所知道的任何语言中最奇怪、最不明显的枚举实现。 甚至 C 也能更好地实现它们。 iota 的东西看起来像一个黑客。 无论如何,iota到底是什么意思? 我应该如何记住该关键字? Go 应该很容易学习。 但这只是古怪。

@pofl
虽然我同意 Go 枚举很尴尬,但iota实际上只是一个普通的英文单词:

微塔
_名词_

  1. 数量非常少; 记笔记; 惠特。
  2. 希腊字母的第九个字母(I,ι)。
  3. 这个字母代表的元音。

据推测,他们会根据语言的使用来进行定义一。

在回应此处较早的评论时附带说明:
虽然我也很喜欢 Go 中的有区别的联合,但我觉得它们应该与实际的枚举分开。 随着泛型目前的发展方式,您实际上可能会通过接口中的类型列表获得与可区分联合非常相似的东西。 请参阅#41716。

Go 中iota的使用大致基于它在 APL 中的使用。 引用https://en.wikipedia.org/wiki/Iota

在一些编程语言(例如,A+、APL、C++[6]、Go[7])中,iota(作为小写符号⍳或标识符iota)用于表示和生成一个连续整数数组。 例如,在 APL 中 ⍳4 给出 1 2 3 4。

此页面是否有帮助?
0 / 5 - 0 等级