Go: 提案:规范:添加和类型/有区别的联合

创建于 2017-03-06  ·  320评论  ·  资料来源: golang/go

这是对和类型的提议,也称为可区分联合。 Go 中的 Sum 类型本质上应该像接口一样,除了:

  • 它们是值类型,如结构
  • 其中包含的类型在编译时固定

Sum 类型可以与 switch 语句匹配。 编译器检查所​​有变体是否匹配。 在 switch 语句的分支内,可以像使用匹配的变体一样使用该值。

Go2 LanguageChange NeedsInvestigation Proposal

最有用的评论

感谢您创建此提案。 我一直在玩这个想法一年左右。
以下是我所得到的具体建议。 我认为
“选择类型”实际上可能比“总和类型”更好,但 YMMV。

Go 中的求和类型

一个和类型由两个或多个类型加上“|”表示
操作员。

type: type1 | type2 ...

结果类型的值只能包含指定类型之一。 这
类型被视为接口类型 - 它的动态类型是
分配给它的值。

作为一种特殊情况,“nil”可用于指示该值是否可以
变成零。

例如:

type maybeInt nil | int

sum类型的方法集持有方法集的交集
它的所有组件类型,不包括任何具有相同
名字,但签名不同。

像任何其他接口类型一样,和类型可能是动态的主题
类型转换。 在类型开关中,开关的第一臂
匹配存储的类型将被选择。

和类型的零值是第一个类型的零值
总和。

将值分配给和类型时,如果该值可以容纳更多
多于一种可能的类型,则选择第一种。

例如:

var x int|float64 = 13

将导致一个动态类型为 int 的值,但是

var x int|float64 = 3.13

将导致动态类型为 float64 的值。

执行

一个简单的实现可以完全像接口一样实现 sum 类型
值。 更复杂的方法可以使用表示
适用于一组可能的值。

例如,一个 sum 类型仅由没有指针的具体类型组成
可以用非指针类型实现,使用额外的值来
记住实际类型。

对于结构总和类型,甚至可以使用备用填充
为此目的,结构共有的字节。

所有320条评论

从开源发布之前开始,这在过去已经讨论过多次。 过去的共识是 sum 类型不会对接口类型增加太多。 一旦你把它全部整理出来,你最终得到的是一个接口类型,编译器会检查你是否已经填写了类型切换的所有情况。 对于新的语言更改来说,这是一个相当小的好处。

如果你想进一步推动这个提案,你需要写一个更完整的提案文档,包括:语法是什么? 确切地说,它们是如何工作的? (你说它们是“值类型”,但接口类型也是值类型)。 有哪些取舍?

我认为这对于 Go1 的类型系统来说变化太大了,没有紧迫的需要。
我建议我们在 Go 2 的更大背景下重新审视这一点。

感谢您创建此提案。 我一直在玩这个想法一年左右。
以下是我所得到的具体建议。 我认为
“选择类型”实际上可能比“总和类型”更好,但 YMMV。

Go 中的求和类型

一个和类型由两个或多个类型加上“|”表示
操作员。

type: type1 | type2 ...

结果类型的值只能包含指定类型之一。 这
类型被视为接口类型 - 它的动态类型是
分配给它的值。

作为一种特殊情况,“nil”可用于指示该值是否可以
变成零。

例如:

type maybeInt nil | int

sum类型的方法集持有方法集的交集
它的所有组件类型,不包括任何具有相同
名字,但签名不同。

像任何其他接口类型一样,和类型可能是动态的主题
类型转换。 在类型开关中,开关的第一臂
匹配存储的类型将被选择。

和类型的零值是第一个类型的零值
总和。

将值分配给和类型时,如果该值可以容纳更多
多于一种可能的类型,则选择第一种。

例如:

var x int|float64 = 13

将导致一个动态类型为 int 的值,但是

var x int|float64 = 3.13

将导致动态类型为 float64 的值。

执行

一个简单的实现可以完全像接口一样实现 sum 类型
值。 更复杂的方法可以使用表示
适用于一组可能的值。

例如,一个 sum 类型仅由没有指针的具体类型组成
可以用非指针类型实现,使用额外的值来
记住实际类型。

对于结构总和类型,甚至可以使用备用填充
为此目的,结构共有的字节。

@rogpeppe这将如何与类型断言和类型切换交互? 据推测,在不是 sum 成员的类型(或对类型的断言)上使用case将是一个编译时错误。 在这种类型上进行非穷尽的切换也是错误的吗?

对于类型开关,如果您有

type T int | interface{}

你也是:

switch t := t.(type) {
  case int:
    // ...

并且 t 包含一个包含 int 的 interface{},它是否与第一种情况匹配? 如果第一种情况是case interface{}怎么办?

或者 sum 类型可以只包含具体类型吗?

type T interface{} | nil呢? 如果你写

var t T = nil

t的类型是什么? 还是禁止施工? type T []int | nil出现了类似的问题,所以它不仅仅是关于接口。

是的,我认为出现编译时错误是合理的
有一个无法匹配的案例。 不确定是不是
允许在这种类型上进行非详尽开关的好主意 - 我们
不需要在其他任何地方详尽无遗。 可能的一件事
不过要好:如果 switch 是详尽无遗的,我们就不需要默认值
使其成为终止声明。

这意味着如果您有以下情况,您可以让编译器出错:

func addOne(x int|float64) int|float64 {
    switch x := x.(type) {
    case int:
        return x + 1
    case float64:
         return x + 1
    }
}

并且您更改总和类型以添加​​额外的案例。

对于类型开关,如果您有

类型 T int | 界面{}

你也是:

开关 t := t.(type) {
案例诠释:
// ...
并且 t 包含一个包含 int 的 interface{},它是否与第一种情况匹配? 如果第一种情况是 case interface{} 呢?

t 不能包含包含 int 的 interface{}。 t 是一个接口
type 就像任何其他接口类型一样,除了它只能
包含它所包含的枚举类型集。
就像 interface{} 不能包含包含 int 的 interface{}。

Sum 类型可以匹配接口类型,但它们仍然只是得到一个具体的
输入动态值。 例如,最好有:

type R io.Reader | io.ReadCloser

T 型接口怎么样{} | 零? 如果你写

var t T = 零

t的类型是什么? 还是禁止施工? 类型 T []int | 也会出现类似的问题。 nil,所以它不仅仅是关于接口。

根据上面的建议,你得到第一项
在可以分配值的总和中,所以
你会得到 nil 接口。

事实上接口{} | nil 在技术上是多余的,因为任何接口{}
可以为零。

对于 []int | nil,nil []int 与 nil 接口不同,所以
([]int|nil)(nil)具体值将是[]int(nil)不是无类型的nil

[]int | nil情况很有趣。 我希望类型声明中的nil始终表示“nil 接口值”,在这种情况下

type T []int | nil
var x T = nil

意味着x是 nil 接口,而不是 nil []int

该值将与以相同类型编码的 nil []int不同:

var y T = []int(nil)  // y != x

即使总和是所有值类型,也不会总是需要 nil 吗? 否则var x int64 | float64会是什么? 从其他规则推断,我的第一个想法是第一种类型的零值,但是var x interface{} | int呢? 正如@bcmills指出的那样,它必须是一个不同的总和为零。

这似乎过于微妙。

详尽的类型开关会很好。 当它不是所需的行为时,您始终可以添加一个空的default:

提案说“当给一个总和类型赋值时,如果该值可以容纳更多
多于一种可能的类型,则选择第一种。”

所以,与:

type T []int | nil
var x T = nil

x 将具有具体类型 []int,因为 nil 可分配给 []int,而 []int 是该类型的第一个元素。 它等于任何其他 []int (nil) 值。

即使总和是所有值类型,也不会总是需要 nil 吗? 否则什么会 var x int64 | float64 是?

提案说“和类型的零值是第一个类型的零值
总和。”,所以答案是 int64(0)。

从其他规则推断,我的第一个想法是第一种类型的零值,但是 var x interface{} | 内部? 正如@bcmills指出的那样,它必须是一个不同的总和 nil

不,在这种情况下,它只是通常的接口 nil 值。 该类型 (interface{} | nil) 是多余的。 也许让它成为一个编译器来指定一个元素是另一个元素的超集的总和类型可能是一个好主意,因为我目前看不到定义这种类型的任何意义。

和类型的零值是和中第一种类型的零值。

这是一个有趣的建议,但由于 sum 类型必须在某处记录它当前持有的值的类型,我相信这意味着 sum 类型的零值不是全字节为零,这将使其不同于Go 中的所有其他类型。 或者我们可以添加一个例外,说明如果类型信息不存在,则该值是列出的第一个类型的零值,但是如果不存在,我不确定如何表示nil列出的第一种类型。

所以(stuff) | nil只有当 (stuff) 中的任何内容都可以为零时才有意义,而nil | (stuff)含义取决于东西中的任何内容是否可以为零? nil 增加了什么值?

@ianlancetaylor我相信许多函数式语言实现(封闭)和类型本质上就像你在 C 中

struct {
    int which;
    union {
         A a;
         B b;
         C c;
    } summands;
}

如果which按顺序索引到联合的字段中,0 = a, 1 = b, 2 = c,则零值定义会导致所有字节都为零。 并且您需要将类型存储在其他地方,这与接口不同。 在存储类型信息的任何地方,您还需要对某种类型的 nil 标签进行特殊处理。

这将使联合的值类型而不是特殊的接口,这也很有趣。

如果记录类型的字段具有表示第一种类型的零值,是否有办法使全零值工作? 我假设一种可能的表示方式是:

type A = B|C
struct A {
  choice byte // value 0 or 1
  value ?// (thing big enough to store B | C)
}

[编辑]

抱歉@jimmyfrasche打我一拳。

有什么不能用 nil 添加的东西吗

type S int | string | struct{}
var None struct{}

?

这似乎避免了很多混乱(至少我有)

或更好

type (
     None struct{}
     S int | string | None
)

这样你就可以在None上输入 switch 并用None{}分配

@jimmyfrasche struct{}不等于nil 。 这是一个小细节,但它会使总和上的类型切换不必要地(?)与其他类型上的类型切换不同。

@bcmills这不是我的意图 - 我的意思是它可以用于区分缺乏价值的相同目的,而不会与总和中任何类型的 nil 的含义重叠。

@rogpeppe这个打印什么?

// r is an io.Reader interface value holding a type that also implements io.Closer
var v io.ReadCloser | io.Reader = r
switch v.(type) {
case io.ReadCloser: fmt.Println("ReadCloser")
case io.Reader: fmt.Println("Reader")
}

我会假设“读者”

@jimmyfrasche我会假设ReadCloser ,与您从任何其他界面上的类型开关中获得的相同。

(而且我还希望仅包含接口类型的总和使用的空间不会超过常规接口,尽管我认为显式标记可以在类型切换中节省一些查找开销。)

@bcmills这是有趣的https :

@ianlancetaylor这是一个很好的提出点,谢谢。 不过,我认为这并不难,尽管这确实意味着我的“天真的实施”建议本身就太天真了。 和类型虽然被视为接口类型,但实际上不必包含指向该类型及其方法集的直接指针 - 相反,它可以在适当的时候包含一个暗示该类型的整数标记。 即使类型本身为零,该标记也可能是非零的。

鉴于:

 var x int | nil = nil

x 的运行时值不必全为零。 打开 x 的类型或转换时
到另一种接口类型,标签可以通过一个包含
实际的类型指针。

另一种可能性是仅当它是第一个元素时才允许 nil 类型,但是
这排除了以下结构:

var t nil | int
var u float64 | t

@jimmyfrasche我会假设 ReadCloser,与您从任何其他界面上的类型开关中获得的相同。

是的。

@bcmills这是有趣的https :

我不明白这个。 为什么“这 [...] 必须对类型开关有效才能打印 ReadCloser”
与任何接口类型一样,sum 类型仅存储其中内容的具体值。

当总和中有多个接口类型时,运行时表示只是一个接口值——只是我们知道底层值必须实现一个或多个声明的可能性。

也就是说,当您为 I1 和 I2 都是接口类型的类型 (I1 | I2) 分配某些内容时,以后无法判断您放入的值当时是否已知实现 I1 或 I2。

如果你的类型是 io.ReadCloser | io.Reader 你不能确定当你在 io.Reader 上键入 switch 或断言它不是 io.ReadCloser 除非分配给 sum 类型取消装箱并重新装箱接口。

反过来说,如果你有 io.Reader | io.ReadCloser 它要么永远不会接受 io.ReadCloser,因为它严格从右到左,要么实现必须从总和中的所有接口中搜索“最佳匹配”接口,但不能很好地定义。

@rogpeppe在您的提案中,忽略了实现中的优化可能性和零值的微妙之处,使用 sum 类型而不是手动制作的接口类型(包含相关方法的交集)的主要好处是类型检查器可以指出错误在编译时而不是运行时。 第二个好处是类型的值更具区别性,因此可能有助于程序的可读性/理解。 还有其他主要好处吗?

(我不是想以任何方式削弱提案,只是想让我的直觉正确。特别是如果额外的句法和语义复杂性“相当小”——不管这意味着什么——我可以明确地看到拥有编译器的好处尽早发现错误。)

@griesemer是的,没错。

特别是在通过通道或网络传递消息时,我认为能够拥有一种准确表达可用可能性的类型有助于可读性和正确性。 目前,通过在接口类型中包含未导出的方法来半心半意地尝试这样做是很常见的,但这是 a) 可以通过嵌入来规避和 b) 很难看到所有可能的类型,因为未导出的方法是隐藏的。

@jimmyfrasche

如果你的类型是 io.ReadCloser | io.Reader 你不能确定当你在 io.Reader 上键入 switch 或断言它不是 io.ReadCloser 除非分配给 sum 类型取消装箱并重新装箱接口。

如果你有那种类型,你就知道它总是一个 io.Reader(或 nil,因为任何 io.Reader 也可以是 nil)。 这两个替代方案不是互斥的 - 提议的总和类型是“包含或”而不是“互斥或”。

反过来说,如果你有 io.Reader | io.ReadCloser 它要么永远不会接受 io.ReadCloser,因为它严格从右到左,要么实现必须从总和中的所有接口中搜索“最佳匹配”接口,但不能很好地定义。

如果通过“走另一条路”,你的意思是分配给那种类型,提案说:

“当给一个和类型赋值时,如果该值可以容纳更多
多于一种可能的类型,则选择第一种。”

在这种情况下,io.ReadCloser 可以同时适用于 io.Reader 和 io.ReadCloser,因此它选择了 io.Reader,但实际上无法分辨。 io.Reader 类型和 io.Reader | 类型之间没有可检测的差异。 io.ReadCloser,因为io.Reader 也可以容纳所有实现io.Reader 的接口类型。 这就是为什么我怀疑让编译器拒绝这样的类型可能是个好主意。 例如,它可以拒绝任何涉及 interface{} 的 sum 类型,因为 interface{} 已经可以包含任何类型,因此额外的限定不会添加任何信息。

@rogpeppe关于你的提议,我有很多喜欢的地方。 从左到右的赋值语义和零值是最左边类型的零值规则非常清晰简单。 非常去。

我担心的是将接口中已装箱的值分配给 sum 类型变量。

现在,让我们使用我之前的示例并说 RC 是一个可以分配给 io.ReadCloser 的结构体。

如果你这样做

var v io.ReadCloser | io.Reader = RC{}

结果显而易见。

然而,如果你这样做

var r io.Reader = RC{}
var v io.ReadCloser | io.Reader = r

唯一明智的做法是让 v 将 r 存储为 io.Reader,但这意味着当您键入 switch on v 时,您无法确定当您点击 io.Reader 案例时您实际上没有io.ReadCloser。 你需要有这样的东西:

switch v := v.(type) {
case io.ReadCloser: useReadCloser(v)
case io.Reader:
  if rc, ok := v.(io.ReadCloser); ok {
    useReadCloser(rc)
  } else {
    useReader(v)
  }
}

现在,在某种意义上,io.ReadCloser <: io.Reader,你可以按照你的建议禁止那些,但我认为这个问题更基本,可能适用于 Go† 的任何总和类型提案。

假设您有三个接口 A、B 和 C,分别具有方法 A()、B() 和 C(),以及一个具有所有三种方法的结构 ABC。 A、B 和 C 不相交,所以 A | 乙 | C 及其排列都是有效的类型。 但你仍然有这样的情况

var c C = ABC{}
var v A | B | C = c

有很多方法可以重新排列它,但当涉及接口时,您仍然无法获得关于 v 是什么的有意义的保证。 如果订单很重要,则在拆箱总和后,您需要拆箱界面。

也许限制应该是所有被加数都不能是接口?

我能想到的唯一其他解决方案是禁止将接口分配给 sum 类型的变量,但这似乎以自己的方式更严重。

† 这不涉及用于消除歧义的总和中的类型的类型构造函数(就像在 Haskell 中,您必须说 Just v 来构造一个类型为 Maybe 的值)——但我根本不赞成这样做。

@jimmyfrasche有序拆箱的用例真的很重要吗? 这不是明显对我来说,和它重要的情况下,可以很容易地解决与明确的盒子结构:

type ReadCloser struct {  io.ReadCloser }
type Reader struct { io.Reader }

var v ReadCloser | Reader = Reader{r}

@bcmills更重要的是,结果并不明显和繁琐,这意味着当涉及到接口时,您想要使用 sum 类型的所有保证都会消失。 我可以看到它会导致各种微妙的错误和误解。

您提供的显式框结构示例表明,在 sum 类型中禁止接口根本不会限制 sum 类型的功能。 它有效地创建了我在脚注中提到的消除歧义的类型构造函数。 诚然,这有点烦人,而且是一个额外的步骤,但它很简单,而且感觉非常符合 Go 的理念,即让语言结构尽可能正交。

总和类型的所有保证

这取决于您期望的保证。 我认为你期望总和类型是
一个严格标记的值,所以给定任何类型 A|B|C,你确切地知道什么是静态的
您分配给它的类型。 我将其视为对单个具体值的类型限制
type - 限制是该值与(至少)A、B 和 C 之一类型兼容。
最后它只是一个带有值的接口。

也就是说,如果一个值可以分配给一个 sum 类型,因为它是赋值兼容的
使用 sum 类型的成员之一,我们不记录这些成员中的哪一个
“选择” - 我们只记录值本身。 与分配 io.Reader 时相同
到接口{},您将丢失静态 io.Reader 类型,而只有值本身
它与 io.Reader 兼容,但也与它发生的任何其他接口类型兼容
来实施。

在你的例子中:

var c C = ABC{}
var v A | B | C = c

对 A、B 和 C 中任何一个的 v 类型断言都会成功。 这对我来说似乎是合理的。

@rogpeppe这些语义比我想象的更有意义。 我仍然不完全相信接口和总和可以很好地混合,但我不再确定它们不是。 进步!

假设您有type U I | *T ,其中I是接口类型,而*T是实现I

给定的

var i I = new(T)
var u U = i

u的动态类型是*T ,在

var u U = new(T)

您可以使用类型断言将*T作为I 。 那是对的吗?

这意味着从有效接口值到总和的赋值必须搜索总和中的第一个匹配类型。

这也将是从像有些不同var v uint8 | int32 | int64 = i这会,我想,只是一直用这三种类型中的哪去i是即使i是一个int64可以放入uint8

进步!

好极了!

您可以使用类型断言将 *T 作为 I 访问。 那是对的吗?

是的。

这意味着从有效接口值到总和的赋值必须搜索总和中的第一个匹配类型。

是的,正如提案所说(当然,编译器静态地知道要选择哪个,因此在运行时无需搜索)。

它也与 var v uint8 | 之类的东西有些不同。 int32 | int64 = i 我想,即使我是一个可以放入 uint8 的 int64,我也总是使用这三种类型中的任何一种。

是的,因为除非 i 是一个常数,否则它只能分配给这些选项之一。

是的,因为除非 i 是一个常数,否则它只能分配给这些选项之一。

我意识到这并不完全正确,因为规则允许将未命名类型分配给命名类型。 我不认为这有太大区别。 规则保持不变。

所以我上一篇文章中的I | *T类型实际上与I类型相同,而io.ReadCloser | io.Reader实际上与io.Reader类型相同?

这是正确的。 我建议的规则将涵盖这两种类型,即编译器拒绝 sum 类型,其中一种类型是由另一种类型实现的接口。 相同或相似的规则可以涵盖具有重复类型的 sum 类型,例如int|int

一个想法: int|bytebyte|int可能是不直观的,但在实践中可能没问题。

这意味着从有效接口值到总和的赋值必须搜索总和中的第一个匹配类型。

是的,正如提案所说(当然,编译器静态地知道要选择哪个,因此在运行时无需搜索)。

我不关注这个。 我读它的方式(可能与预期不同)至少有两种方法可以处理 I 和 T-implements-I 的联合 U。

1a) 在分配U u = t ,标签被设置为 T。后来的选择结果是 T,因为标签是 T。
1b) 在赋值U u = i (i 真的是一个 T)时,标签被设置为 I。后来的选择结果是一个 T,因为标签是一个 I但是第二次检查(因为 T 实现了 I 和 T是 U) 的成员发现了一个 T。

2a) 像 1a
2b) 在U u = i赋值(i 实际上是 T)时,生成的代码检查值 (i) 以查看它是否实际上是 T,因为 T 实现了 I 并且 T 也是 U 的成员。因为也就是说,标签被设置为 T。后来的选择直接导致一个 T。

在 T、V、W 都实现 I 和U = *T | *V | *W | I ,赋值U u = i需要(最多)3 次类型测试。

但是,接口和指针并不是联合类型的原始用例,是吗?

我可以想象某些类型的hackery,其中“不错”的实现会执行一些位敲击——例如,如果您有 4 个或更少的指针类型的联合,其中所有引用都是 4 字节对齐的,请将标签存储在较低的 2位值。 这反过来意味着取联合成员的地址是不好的(无论如何都不会,因为该地址可用于重新存储“旧”类型而无需调整标签)。

或者,如果我们有一个 50 位左右的地址空间,并且愿意对 NaN 采取一些自由,我们可以将整数、指针和双精度都打成一个 64 位联合,以及一些位摆弄的可能成本。

这两个子建议都很粗暴,我确信两者都会有一小部分(?)狂热的支持者。

这反过来意味着取工会成员的地址是不好的

正确的。 但我不认为类型断言的结果在今天是可寻址的,是吗?

在分配 U u = i(i 实际上是 T)时,标签设置为 I。

我认为这是症结所在——没有标签 I。

暂时忽略运行时表示并将 sum 类型视为接口。 与任何接口一样,它具有动态类型(存储在其中的类型)。 您所指的“标签”正是那种动态类型。

正如你所建议的(我试图在提案的最后一段暗示),可能有比使用指向运行时类型的指针更有效的方式来存储类型标签,但最终它总是只是编码动态sum-type 值的类型,而不是创建时“选择”了哪个替代项。

但是,接口和指针并不是联合类型的原始用例,是吗?

事实并非如此,但在我看来,任何提案都需要与其他语言功能尽可能正交。

@dr2chase 到目前为止我的理解是,如果 sum 类型在其定义中包含任何接口类型,那么在运行时它的实现与接口相同(包含方法集的交集),但关于允许类型的编译时不变量仍然是强制执行。

即使 sum 类型只包含具体类型并且它像 C 风格的判别联合一样实现,你也不能在 sum 类型中寻址一个值,因为该地址在你取后可能变成不同的类型(和大小)地址。 不过,您可以获取总和类型值本身的地址。

求和类型的行为是否可取? 我们可以很容易地声明所选/断言类型与程序员在将值分配给联合时所说/暗示的相同。 否则我们可能会被引向有趣的地方,比如 int8、int16 和 int32 等等。或者,例如int8 | uint8

求和类型的行为是否可取?

这是一个判断问题。 我相信是这样,因为我们在语言中已经有了接口的概念——具有静态和动态类型的值。 在某些情况下,所提议的 sum 类型只是提供了一种更精确的方式来指定接口类型。 这也意味着 sum 类型可以在不受任何其他类型限制的情况下工作。 如果不这样做,则需要排除接口类型,然后该功能就不是完全正交的。

否则我们可能会被引向有趣的地方,例如 int8、int16 和 int32 等。或者,例如 int8 | uint8。

你在这里有什么顾虑?

您不能将函数类型用作地图的键类型。 我并不是说那是等价的,只是有先例可以限制其他类型的类型。 仍然开放允许接口,仍然没有出售。

您可以使用包含您无法使用的接口的 sum 类型编写什么样的程序?

反建议。

联合类型是一种列出零个或多个类型的类型,写成

union {
  T0
  T1
  //...
  Tn
}

联合中列出的所有类型(T0、T1、...、Tn)必须不同,并且没有一个可以是接口类型。

方法可以通过通常的规则在定义的(命名的)联合类型上声明。 列出的类型不会提升任何方法。

联合类型没有嵌入。 在另一种中列出一种联合类型与列出任何其他有效类型相同。 然而,联合不能递归地列出它自己的类型,因为type S struct { S }是无效的。

联合可以嵌入到结构中。

联合类型的值是一种动态类型,仅限于列出的类型之一,以及动态类型的值——据说是存储的值。 列出的类型之一始终是动态类型。

空联合的零值是唯一的。 非空联合的零值是联合中列出的第一种类型的零值。

联合类型的值U可以使用U{}来创建零值。 如果U有一个或多个类型,并且v是所列类型之一的值, TU{v}创建一个联合值存储v与动态类型T 。 如果v的类型未在U中列出并且可以分配给多个列出的类型,则需要进行显式转换以消除歧义。

联合类型U值可以转换为另一个联合类型V如在V(U{})如果U中的类型集是V组类型。 也就是说,忽略顺序, U必须具有与V相同的所有类型,并且U不能具有不在V但在V可以有不在U

联合类型之间的可分配性定义为可转换性,只要最多定义(命名)一种联合类型即可。

可以将联合类型U的所列类型之一T值分配给联合类型U的变量。 这将动态类型设置为T并存储该值。 分配兼容值的工作方式如上。

如果所有列出的类型都支持相等运算符:

  • 相等运算符可用于相同联合类型的两个值。 如果动态类型不同,则联合类型的两个值永远不会相等。
  • 该联合的值可以与其列出的任何类型的值进行比较。 如果联合的动态类型不是另一个操作数的类型,则==为假, !=为真,无论存储的值如何。 分配兼容值的工作方式如上。
  • 联合可以用作映射键

联合类型的值不支持其他运算符。

如果断言类型是动态类型,则针对其列出的类型之一的联合类型的类型断言成立。

如果接口类型的动态类型实现了该接口,则针对接口类型的联合类型的类型断言成立。 (值得注意的是,如果所有列出的类型都实现了这个接口,则断言始终成立)。

类型开关必须是详尽无遗的,包括所有列出的类型,或者包含默认情况。

类型断言和类型开关返回存储值的副本。

包反射需要一种方法来获取反射联合值的动态类型和存储值,以及一种获取反射联合类型的列出类型的方法。

笔记:

部分选择union{...}语法是为了与该线程中的 sum 类型提案区分开来,主要是为了保留 Go 语法中的 nice 属性,并顺便强调这是一个可区分的联合。 因此,这允许有些奇怪的联合,例如union{}union{ int } 。 第一个在很多意义上等同于struct{} (虽然根据定义是不同的类型),所以它不会添加到语言中,除了添加另一个空类型。 第二个可能更有用。 例如, type Id union { int }type Id struct { int }非常相似,除了联合版本允许直接赋值而无需指定idValue.int使其看起来更像是内置类型。

处理赋值兼容类型时所需的消歧转换有点苛刻,但如果更新联合以引入下游代码未准备好的歧义,则会捕获错误。

缺少嵌入是允许使用联合方法并需要在类型切换中进行详尽匹配的结果。

允许联合本身的方法而不是采用列出类型的方法的有效交集,避免意外获得不需要的方法。 当需要提升时,将存储的值类型断言到公共接口允许简单、显式的包装方法。 例如,在联合类型U所有列出的类型都实现fmt.Stringer

func (u U) String() string {
  return u.(fmt.Stringer).String()
}

在链接的 reddit 线程中,rsc 说:

sum { X; 的零值会很奇怪 Y } 不同于 sum { Y; X }。 这不是总和通常的工作方式。

我一直在考虑这个问题,因为它确实适用于任何提案。

这不是错误:这是一个功能。

考虑

type (
  Undefined = struct{}
  UndefinedOrInt union { Undefined; int }
)

对比

type (
  Illegal = struct{}
  IntOrIllegal union { int; Illegal }
)

UndefinedOrInt表示默认情况下它还没有定义,但是当它定义时,它将是一个int值。 这类似于*int现在需要在 Go 中表示和类型 (1 + int) 并且零值也类似。

另一方面, IntOrIllegal表示默认情况下它是 int 0,但它可能在某些时候被标记为非法。 这仍然类似于*int但零值更能表达意图,例如强制它默认为new(int)

这有点像能够将结构中的 bool 字段设为负值,因此零值是您想要的默认值。

和的两个零值本身都是有用和有意义的,程序员可以选择最适合这种情况的值。

如果总和是一周中的某天枚举(每一天都是定义的struct{} ),则首先列出的是一周的第一天,对于iota样式的枚举也是如此。

此外,我不知道任何具有总和类型或具有零值概念的可区分/标记联合的语言。 C 将是最接近的,但零值是未初始化的内存——几乎没有线索可循。 我相信 Java 默认为 null,但那是因为一切都是引用。 我所知道的所有其他语言都有用于被加数的强制类型构造函数,因此实际上并没有零值的概念。 有这样的语言吗? 它有什么作用?

如果问题与数学概念“sum”和“union”的不同,我们总是可以称它们为别的东西(例如“variant”)。

对于名称:Union 混淆了 c/c++ 纯粹主义者。 COBRA 和 COM 程序员主要熟悉 Variant,因为函数式语言似乎更喜欢区分联合。 Set 是动词和名词。 我喜欢关键字 _pick_。 Limbo 使用 _pick_。 它很简短,描述了从有限类型集合中选择类型的意图。

名称/语法在很大程度上是无关紧要的。 挑就好了。

该线程中的任何一个提议都符合集合论定义。

第一种是零值的特殊类型是不相关的,因为类型理论和交换,所以顺序是不相关的(A + B = B + A)。 我的建议保留了该属性,但产品类型在理论上也可以通勤,并且在实践中被大多数语言(包括 Go)认为是不同的,所以它可能不是必需的。

@jimmyfrasche

我个人认为不允许接口作为“选择”成员是一个很大的缺点。 首先,它会完全打败“pick”类型的一个很好的用例——有一个错误是成员之一。 或者,如果您不想强制用户事先使用 StringReader,您想要处理具有 io.Reader 或字符串的选择类型。 但总而言之,接口只是另一种类型,我认为“pick”成员不应该有类型限制。 在这种情况下,如果一个选择类型有 2 个接口成员,其中一个被另一个完全包围,这应该是一个编译时错误,如前所述。

我从你的反建议中喜欢的是可以在选择类型上定义方法的事实。 我不认为它应该提供成员方法的横截面,因为我认为不会有很多方法属于所有成员的情况(无论如何你都有接口)。 一个详尽的 switch + default case 是一个非常好的主意。

@rogpeppe @jimmyfrasche我在你的提案中没有看到的是我们为什么要这样做。 添加一种新类型有一个明显的缺点:这是一个新概念,每个学习 Go 的人都必须学习。 什么是补偿优势? 特别是,新的类型给了我们什么,而我们不能从接口类型中得到什么?

@ianlancetaylor Robert 在这里总结得很好: https :

@ianlancetaylor
归根结底,它使代码更具可读性,这是 Go 的主要指令。 考虑 json.Token,它目前被定义为 interface{},但是文档指出它实际上只能是特定数量的类型之一。 另一方面,如果它被写成

type Token Delim | bool | float64 | Number | string | nil

用户将能够立即看到所有可能性,并且该工具将能够自动创建一个详尽的开关。 此外,编译器还会防止您在其中粘贴意外类型。

归根结底,它使代码更具可读性,这是 Go 的主要指令。

更多功能意味着人们必须了解更多才能理解代码。 对于语言知识一般的人来说,其可读性必然与[新增]特征的数量成反比。

@cznic

更多功能意味着人们必须了解更多才能理解代码。

不总是。 如果您可以将“更多地了解语言”替换为“更多地了解代码中记录不佳或不一致的不变量”,那仍然是一个净赢。 (也就是说,全球知识可以取代对本地知识的需求。)

如果更好的编译时类型检查确实是唯一的好处,那么我们可以通过引入由 vet 检查的注释来获得非常相似的好处,而无需更改语言。 就像是

//vet:types Delim | bool | float64 | Number | string | nil
type Token interface{}

现在,我们目前没有任何形式的兽医评论,所以这不是一个完全严肃的建议。 但我对基本思想很认真:如果我们获得的唯一优势是我们可以完全使用静态分析工具完成的事情,那么是否真的值得在语言中添加一个复杂的新概念?

由 cmd/vet 完成的许多(也许是所有)测试都可以添加到语言中,从某种意义上说,它们可以由编译器而不是单独的静态分析工具进行检查。 但是出于各种原因,我们发现将 vet 与编译器分开是很有用的。 为什么这个概念属于语言方面而不是兽医方面?

@ianlancetaylor重新检查评论: https :

@ianlancetaylor就更改是否合理而言,我一直在积极地忽略这一点——或者更确切地说是将其

如果更好的编译时类型检查确实是唯一的好处,那么我们可以通过引入由 vet 检查的注释来获得非常相似的好处,而无需更改语言。

这仍然容易受到需要学习新事物的批评。 如果我必须学习那些神奇的 vet 评论来调试/理解/使用代码,这是一种心理负担,无论我们是将它分配给 Go 语言预算还是技术上非 Go 语言的预算。 如果有的话,魔术评论的成本更高,因为当我认为我学会了这门语言时,我不知道我需要学习它们。

@cznic
我不同意。 根据您当前的假设,您无法确定一个人会了解什么是渠道,甚至是什么功能。 然而这些东西存在于语言中。 一个新特性并不自动意味着它会使语言变得更难。 在这种情况下,我认为它实际上会让它更容易理解,因为它让读者立即清楚类型应该是什么,而不是使用黑盒 interface{} 类型。

@ianlancetaylor
我个人认为这个特性更多地是为了让代码更容易阅读和推理。 编译时安全是一个非常好的特性,但不是主要特性。 它不仅会使类型签名立即变得更加明显,而且其后续使用也会更容易理解,也更容易编写。 如果人们收到他们没有预料到的类型,他们将不再需要求助于恐慌——即使在标准库中,这也是当前的行为,因此虽然可以更轻松地考虑使用情况,而不会受到未知的阻碍. 我认为为此依赖注释和其他工具(即使它们是第一方的)不是一个好主意,因为更简洁的语法比这样的注释更具可读性。 注释是无结构的,更容易搞砸。

@ianlancetaylor

为什么这个概念属于语言方面而不是兽医方面?

您可以将相同的问题应用于图灵完备核心之外的任何功能,并且可以说我们不希望 Go 成为“图灵油田”。 另一方面,我们确实有一些语言的例子,它们将实际语言的重要子集推到了通用的“扩展”语法中。 (例如,Rust、C++ 和 GNU C 中的“属性”。)

将特性放在扩展或属性中而不是放在核心语言中的主要原因是保持语法兼容性,包括与不知道新特性的工具的兼容性。 (“与工具的兼容性”在实践中是否真正起作用在很大程度上取决于该功能的实际作用。)

在 Go 的上下文中,将特性放入vet主要原因似乎是实现更改,如果应用于语言本身,则不会保留 Go 1 兼容性。 我不认为这是一个问题。

将特性放入vet是它们是否需要在编译期间传播。 例如,如果我写:

switch x := somepkg.SomeFunc().(type) {
…
}

对于不在总和中的类型,跨越包边界,我会得到正确的警告吗? 对我来说vet可以做那么深的传递分析对我来说并不明显,所以也许这就是它需要进入核心语言的原因。

@dr2chase一般来说,当然,您是正确的,但是对于这个特定示例,您是否正确? 代码完全可以理解,无需知道魔术注释是什么意思。 神奇的注释不会以任何方式改变代码的作用。 来自兽医的错误信息应该很清楚。

@bcmills

为什么这个概念属于语言方面而不是兽医方面?

您可以将相同的问题应用于图灵完备核心之外的任何功能......

我不同意。 如果讨论中的特性影响了编译后的代码,那么就会自动有一个支持它的论据。 在这种情况下,该功能显然不会影响编译后的代码。

(而且,是的,vet 可以解析导入包的来源。)

我并不是要声称我关于兽医的论点是决定性的。 但是每一次语言的变化都是从一个消极的位置开始的:简单的语言是非常非常可取的,而像这样一个重要的新特性不可避免地使语言变得更加复杂。 你需要强有力的论据来支持语言改变。 从我的角度来看,这些强有力的论点还没有出现。 毕竟这个问题我们想了很久,还是一个FAQ(https://golang.org/doc/faq#variant_types)。

@ianlancetaylor

在这种情况下,该功能显然不会影响编译后的代码。

我认为这取决于具体细节? @jimmyfrasche上面提到的“和的零值是第一种类型的零值”行为(https://github.com/golang/go/issues/19412#issuecomment-289319916)当然会。

@urandom我正在写一个很长的解释为什么接口和联合类型在没有显式类型构造函数的情况下不能混合,但后来我意识到有一种明智的方法可以做到这一点,所以:

对我的反建议的快速而肮脏的反建议。 (任何未明确提及的内容与我之前的提议相同)。 我不确定一个提议是否比另一个更好,但是这个提议允许接口并且更加明确:

联合有明确的“字段名称”,以下称为“标签名称”:

union { //or whatever
  None, invalid struct{} //None is zero value
  Good, Bad int
  Err error //okay because it's explicitly named
}

仍然没有嵌入。 没有标签名称的类型总是一个错误。

联合值具有动态标记而不是动态类型。

文字价值创造: U{v}只有在完全明确的情况下才有效,否则它必须是U{Tag: v}

可转换性和分配兼容性也会考虑标签名称。

分配给联合并不是魔法。 它总是意味着分配一个兼容的联合值。 要设置存储值,必须明确使用所需的标记名称: v.Good = 1将动态标记设置为 Good,存储值设置为 1。

访问存储的值使用标签断言而不是类型断言:

g := v.[Tag] //may panic
g, ok := v.[Tag] //no panic but could return zero-value, false

v.Tag 是 rhs 上的错误,因为它不明确。

标签开关类似于类型开关,写成switch v.[type] ,除了 case 是联合的标签。

类型断言适用于动态标签的类型。 类型开关的工作方式类似。

给定某种联合类型的值 a、b,如果它们的动态标签相同且存储的值相同,则 a == b。

检查存储的值是否是某个特定值需要标记断言。

如果标记名称未导出,则只能在定义联合的包中设置和访问它。 这意味着具有混合导出和未导出标签的联合的标签切换永远不会在没有默认情况下的定义包之外详尽无遗。 如果所有标签都未导出,则它是一个黑匣子。

反射也需要处理标签名称。

e:对嵌套联合的说明。 给定的

type U union {
  A union {
    A1 T1
    A2 T2
  }
  B union {
    B1 T3
    B2 T4
  }
}
var u U

u 的值是动态标签 A,存储的值是与动态标签 A1 的匿名联合,其存储的值是 T1 的零值。

u.B.B2 = returnsSomeT3()

这是将 u 从零值切换所必需的全部内容,即使它从嵌套联合之一移动到另一个联合,因为它都存储在一个内存位置。 但

v := u.[A].[A2]

有两次恐慌的机会,因为它在两个联合值上标记断言,并且标记断言的 2 值版本在不拆分多行的情况下不可用。 在这种情况下,嵌套标签开关会更干净。

编辑 2:对类型断言的澄清。

给定的

type U union {
  Exported, unexported int
}
var u U

u.(int)这样的类型断言是完全合理的。 在定义包中,这将始终成立。 但是,如果u在定义包u.(int) ,则在动态标记为unexported时会发生恐慌,以避免泄漏实现细节。 对接口类型的断言也是如此。

@ianlancetaylor以下是此功能如何提供帮助的一些示例:

  1. 一些包(例如go/ast )的核心是一种或多种大金额类型。 如果不了解这些类型,就很难浏览这些包。 更令人困惑的是,有时 sum 类型由带有方法的接口(例如go/ast.Node )表示,其他时候由空接口(例如go/ast.Object.Decl )表示。

  2. 将 protobuf oneof功能编译为 Go 会导致未导出的接口类型,其唯一目的是确保对 oneof 字段的分配是类型安全的。 这反过来需要为 oneof 的每个分支生成一个类型。 最终产品的类型文字很难读写:

    &sppb.Mutation{
               Operation: &sppb.Mutation_Delete_{
                   Delete: &sppb.Mutation_Delete{
                       Table:  m.table,
                       KeySet: keySetProto,
                   },
               },
    }
    

    一些(尽管不是全部)oneofs 可以用和类型表示。

  3. 有时,“可能”类型正是人们所需要的。 例如,许多 Google API 资源更新操作允许更改资源字段的子集。 在 Go 中表达这一点的一种自然方式是通过资源结构的变体,每个字段都有一个“可能”类型。 例如,Google Cloud Storage ObjectAttrs资源看起来像

    type ObjectAttrs struct {
       ContentType string
       ...
    }
    

    为了支持部分更新,包还定义了

    type ObjectAttrsToUpdate struct {
       ContentType optional.String
       ...
    }
    

    optional.String看起来像这样( godoc ):

    // String is either a string or nil.
    type String interface{}
    

    这既难以解释又是类型不安全的,但事实证明它在实践中很方便,因为ObjectAttrsToUpdate文字看起来与ObjectAttrs文字完全一样,同时编码存在。 我希望我们能写

    type ObjectAttrsToUpdate struct {
       ContentType string | nil
       ...
    }
    
  4. 许多函数返回(T, error)与异或语义(T 是有意义的,如果错误为零)。 将返回类型写为T | error将澄清语义,提高安全性,并提供更多的组合机会。 即使我们不能(出于兼容性原因)或不想更改函数的返回值, sum 类型对于携带该值仍然很有用,例如将其写入通道。

诚然, go vet注释可以帮助许多这些情况,但不适用于匿名类型有意义的情况。 我想如果我们有 sum 类型,我们会看到很多

chan *Response | error

这种类型足够短,可以写出多次。

@ianlancetaylor这可能不是一个好的开始,但这里有你可以在 Go1 中已经可以做的所有事情,因为我认为承认和总结这些论点是公平的:

(使用我的最新提案和下面的语法/语义标签。还假设发出的代码基本上类似于我在线程中更早发布的 C 代码。)

Sum 类型与 iota、指针和接口重叠。

物联网

这两种类型大致等效:

type Stoplight union {
  Green, Yellow, Red struct {}
}

func (s Stoplight) String() string {
  switch s.[type] {
  case Green: return "green" //etc
  }
}

type Stoplight int

const (
  Green Stoplight = iota
  Yellow
  Red
)

func (s Stoplight) String() string {
  switch s {
  case Green: return "green" //etc
  }
}

编译器可能会为两者发出完全相同的代码。

在联合版本中, int 变成了一个隐藏的实现细节。 对于 iota 版本,您可以询问什么是 Yellow/Red 或将 Stoplight 值设置为 -42,但对于 union 版本则不行——这些都是在优化过程中可以考虑的编译器错误和不变量。 类似地,您可以编写一个无法解释黄灯的(值)开关,但使用标签开关,您需要一个默认情况来明确说明。

当然,有些事情你可以用 iota 做而你不能用联合类型来做。

指针

这两种类型大致相当

type MaybeInt64 union {
  None struct{}
  Int64 int64
}

type MaybeInt64 *int64

指针版本更紧凑。 联合版本需要一个额外的位(这可能是字大小)来存储动态标签,因此值的大小可能与https://golang.org/pkg/database/sql/相同

联合版本更清楚地记录了意图。

当然,有些事情你可以用指针做,而联合类型却做不到。

接口

这两种类型大致相当

type AB union {
  A A
  B B
}

type AB interface {
  secret()
}
func (A) secret() {}
func (B) secret() {}

联合版本不能通过嵌入来规避。 A 和 B 不需要共同的方法——它们实际上可以是原始类型或具有完全不相交的方法集,如 json.Token 示例@urandom发布。

很容易看出你可以在 AB 联合和 AB 接口中放入什么:定义就是文档(我不得不多次阅读 go/ast 源代码来弄清楚什么是什么)。

AB 联合永远不能为零,并且可以在其组成部分的交集之外给出方法(这可以通过将接口嵌入结构中来模拟,但随后构造变得更加微妙且容易出错)。

当然,有些事情你可以用接口做,而联合类型却做不到。

概括

也许重叠太多了。

在每种情况下,联合版本的主要好处确实是更严格的编译时检查。 你不能做的比你能做的更重要。 对于转换为更强不变量的编译器,它可以用来优化代码。 对于翻译成另一件事的程序员,你可以让编译器担心——它只会告诉你你是否错了。 在界面版本中,至少有重要的文档优势。

可以使用“带有未导出方法的接口”策略来构造 iota 和指针示例的笨拙版本。 但是,就此而言,可以使用具有 func 类型和方法值的map[string]interface{}和(非空)接口来模拟结构。 没有人会,因为它更难,更不安全。

所有这些特性都为语言添加了一些东西,但它们的缺失可以解决(痛苦地,并在抗议下)。

所以我假设这个栏不是要展示一个甚至不能用 Go 近似的程序,而是展示一个用 Go 编写的程序,使用联合比没有联合更容易和干净。 所以还有待证明的是。

@jimmyfrasche

我看不出为什么联合类型应该有命名字段。 名称仅在您想区分相同类型的不同字段时才有用。 但是,联合永远不能有多个相同类型的字段,因为这是毫无意义的。 因此,有名字只是多余的,会导致混乱和更多的打字。

从本质上讲,您的联合类型应该类似于:

union {
    struct{}
    int
    err
}

类型本身将提供可用于分配给联合的唯一标识符,这与结构中嵌入类型用作标识符的方式非常相似。

但是,为了使显式赋值起作用,不能通过将未命名类型指定为成员来创建联合类型,因为语法允许这样的表达式。 例如v.struct{} = struct{}

因此,像原始结构体、联合体和函数这样的类型必须事先命名才能成为联合体的一部分,并成为可分配的。 考虑到这一点,嵌套联合不会有什么特别之处,因为内部联合只是另一种成员类型。

现在,我不确定哪种语法会更好。

[union|sum|pick|oneof] {
    type1
    package1.type2
    ....
}

上面看起来更像go,但对于这种类型来说有点冗长。

另一方面, type1 | package1.type2可能看起来不像你通常的 go 类型,但是它获得了使用“|”的好处符号,主要被识别为 OR。 它减少了冗长而不是神秘。

@urandom如果您没有“标签名称”但允许接口将总和折叠为interface{}并进行额外检查。 它们不再是总和类型,因为您可以放入一件事,但可以通过多种方式将其取出。 标签名称让它们成为和类型并保持接口没有歧义。

不过,标签名称修复的不仅仅是接口问题。{} 它们使类型变得不那么神奇,让一切都变得光彩夺目,而不必为了区分而发明一堆类型。 正如您所指出的,您可以使用显式赋值和类型文字。

您可以为一种类型提供多个标签是一项功能。 考虑一种类型来衡量连续发生了多少成功或失败(1 次成功抵消了 N 次失败,反之亦然)

type Counter union {
  Successes, Failures uint 
}

没有您需要的标签名称

type (
  Success uint
  Failures uint
  Counter Successes | Failures
)

并且赋值看起来像c = Successes(1)而不是c.Successes = 1 。 你收获不多。

另一个示例是表示本地或远程故障的类型。 使用标签名称,这很容易建模:

type Failure union {
  Local, Remote error
}

无论实际错误是什么,都可以使用其标记名称指定错误的原因。 如果没有标签名称,您需要type Local { error }和远程相同,即使您允许直接在总和中使用接口。

标签名称有点像在联合中本地创建特殊的别名和命名类型。 拥有多个具有相同类型的“标签”并不是我的提议所独有的:这是每种函数式语言(我所知道的)所做的。

为导出类型创建未导出标签(反之亦然)的能力也是一个有趣的转折。

还具有单独的标记和类型断言允许一些有趣的代码,例如能够使用一行包装器将共享方法提升到联合。

看起来它解决的问题比它引起的要多,并且使一切都更好地结合在一起。 老实说,我在写它的时候并不太确定,但我越来越相信这是解决将 sums 集成到 Go 中的所有问题的唯一方法。

为了稍微扩展一下,激励我的例子来自@rogpeppe io.Reader | io.ReadCloser 。 允许没有标签的接口,这与io.Reader类型相同。

您可以将 ReadCloser 放入并作为阅读器将其拉出。 你输了 A | B 表示和类型的 A 或 B 属性。

如果您需要具体说明有时将io.ReadCloser作为io.Reader您需要创建包装结构,如@bcmills 所指出的, type Reader struct { io.Reader }等,并且类型为Reader | ReadCloser

即使您将总和限制为具有不相交方法集的接口,您仍然会遇到这个问题,因为一种类型可以实现多个接口。 你失去了和类型的明确性:它们不是“A 或 B”:它们是“A 或 B 或有时你喜欢的任何一个”。

更糟糕的是,如果这些类型来自其他包,即使您非常小心地构建程序,它们也会在更新后突然出现不同的行为,以便 A 永远不会与 B 相同。

最初我探索了禁止接口来解决问题。 没有人对此感到满意! 但它也没有摆脱诸如a = b之类的问题,这取决于 a 和 b 的类型,这意味着不同的东西,我对此感到不舒服。 当类型可分配性发挥作用时,还必须有很多关于在选择中选择什么类型的规则。 这是很多魔法。

你添加标签,一切都会消失。

使用union { R io.Reader | RC io.ReadCloser }你可以明确地说我希望这个 ReadCloser 被认为是一个 Reader 如果这是有意义的。 不需要包装类型。 它隐含在定义中。 无论标签的类型如何,它要么是一个标签,要么是另一个。

缺点是,如果你从其他地方得到一个 io.Reader,比如说 chan receive 或 func 调用,它可能是一个 io.ReadCloser,你需要将它分配给正确的标签,你必须在 io 上键入断言。仔细阅读并测试。 但这使程序的意图更加清晰 - 正是您的意思在代码中。

同样因为标签断言与类型断言不同,如果你真的不关心并且只想要一个 io.Reader ,你可以使用类型断言来拉出它,不管标签如何。

这是一个玩具示例的最大努力音译为没有联合/总和/等的 Go。 这可能不是最好的例子,但它是我曾经看到的一个例子。

它以更具操作性的方式显示语义,这可能比提案中的一些简洁要点更容易理解。

音译中有相当多的样板,所以我通常只写了几种方法的第一个实例,并附有关于重复的注释。

在 Go with union 提案中:

type fail union { //zero value: (Local, nil)
  Local, Remote error
}

func (f fail) Error() string {
  //Could panic if local/remote nil, but assuming
  //it will be constructed purposefully
  return f.(error).Error()
}

type U union { //zero value: (A, "")
  A, B, C string
  D, E    int
  F       fail
}

//in a different package

func create() pkg.U {
  return pkg.U{D: 7}
}

func process(u pkg.U) {
  switch u := u.[type] {
  case A:
    handleA(u) //undefined here, just doing something with unboxed value
  case B:
    handleB(u)
  case C:
    handleC(u)
  case D:
    handleD(u)
  case E:
    handleE(u)
  case F:
    switch u := u.[type] {
    case Local:
      log.Fatal(u)
    case Remote:
      log.Printf("remote error %s", u)
      retry()
    } 
  }
}

音译为当前的 Go:

(附注音译与上述的区别)

const ( //simulates tags, namespaced so other packages can see them without overlap
  Fail_Local = iota
  Fail_Remote
)

//since there are only two tags with a single type this can
//be represented precisely and safely
//the error method on the full version of fail can be
//put more succinctly with type embedding in this case

type fail struct { //zero value (Fail_Local, nil) :)
  remote bool
  error
}

// e, ok := f.[Local]
func (f *fail) TagAssertLocal2() (error, bool) { //same for TagAssertRemote2
  if !f.remote {
    return nil, false
  }
  return f.error, true
}

// e := f.[Local]
func (f *fail) TagAssertLocal() error { //same for TagAssertRemote
  if !f.remote {
    panic("invalid tag assert")
  }
  return f.error
}

// f.Local = err
func (f *fail) SetLocal(err error) { //same for SetRemote
  f.remote = false
  f.error = err
}

// simulate tag switch
func (f *fail) TagSwitch() int {
  if f.remote {
    return Fail_Remote
  }
  return Fail_Local
}

// f.(someType) needs to be written as f.TypeAssert().(someType)
func (f *fail) TypeAssert() interface{} {
  return f.error
}

const (
  U_A = iota
  U_B
  // ...
  U_F
)

type U struct { //zero value (U_A, "", 0, fail{}) :(
  kind int //more than two types, need an int
  s string //these would all occupy the same space
  i int
  f fail
}

//s, ok := u.[A]
func (u *U) TagAssertA2() (string, bool) { //similar for B, etc.
  if u.kind == U_A {
    return u.s, true
  }
  return "", false
}

//s := u.[A]
func (u *U) TagAssertA() string { //similar for B, etc.
  if u.kind != U_A {
    panic("invalid tag assert")
  }
  return u.s
}

// u.A = s
func (u *U) SetA(s string) { //similar for B, etc.
  //if there were any pointers or reference types
  //in the union, they'd have to be nil'd out here,
  //since the space isn't shared
  u.kind = U_A
  u.s = s
}

// special case of u.F.Local = err
func (u *U) SetF_Local(err error) { //same for SetF_Remote
  u.kind = U_F
  u.f.SetLocal(err)
}

func (u *U) TagSwitch() int {
  return u.kind
}

func (u *U) TypeAssert() interface{} {
  switch u.kind {
  case U_A, U_B, U_C:
    return u.s
  case U_D, U_E:
    return u.i
  }
  return u.f
}

//in a different package

func create() pkg.U {
  var u pkg.U
  u.SetD(7)
  return u
}

func process(u pkg.U) {
  switch u.TagSwitch() {
  case U_A:
    handleA(u.TagAssertA())
  case U_B:
    handleB(u.TagAssertB())
  case U_C:
    handleC(u.TagAssertC())
  case U_D:
    handleD(u.TagAssertD())
  case U_E:
    handleE(u.TagAssertE())
  case U_F:
    switch u := u.TagAssertF(); u.TagSwitch() {
    case Fail_Local:
      log.Fatal(u.TagAssertLocal())
    case Fail_Remote:
      log.Printf("remote error %s", u.TagAssertRemote())
    }
  }
}

@jimmyfrasche

由于联合包含可能具有相同类型的标签,以下语法是否更适合:

func process(u pkg.U) {
  switch v := u {
  case A:
    handleA(v) //undefined here, just doing something with unboxed value
  case B:
    handleB(v)
  case C:
    handleC(v)
  case D:
    handleD(v)
  case E:
    handleE(v)
  case F:
    switch w := v {
    case Local:
      log.Fatal(w)
    case Remote:
      log.Printf("remote error %s", w)
      retry()
    } 
  }
}

在我看来,当与 switch 一起使用时,联合与 int 或 string 等类型非常相似。 主要区别在于,只有有限的“值”可以分配给它,而不是前一种类型,并且开关本身是详尽无遗的。 因此,在这种情况下,我并没有真正看到需要特殊语法,从而减少了开发人员的脑力劳动。

此外,根据此提案,此类代码是否有效:

type Foo union {
    // Completely different types, no ambiguity
    A string
    B int
}

func Bar(f Foo) {
    switch v := f {
        ....
    }
}

....

func main() {
    // No need for Bar(Foo{A: "hello world"})
    Bar("hello world")
    Bar(1)
}

@urandom我选择了一种语法来反映语义,尽可能使用与现有 Go 语法的类比。

使用接口类型你可以做

var i someInterface = someValue //where someValue implements someInterface.
var j someInterface = i //this assignment is different from the last one.

这很好而且很明确,因为只要满足合同, someValue的类型是什么并不重要。

当您在联合上引入标签† 时,有时会产生歧义。 魔术分配仅在某些情况下有效。 特殊的外壳它只会让您有时必须明确。

我认为有时可以跳过一个步骤没有意义,尤其是当代码更改很容易使该特殊情况无效时,无论如何您都必须返回并更新所有代码。 要使用您的 Foo/Bar 示例,如果将C int添加到FooBar(1)必须更改但不是Bar("hello world") 。 在可能不那么常见的情况下保存一些击键会使一切变得复杂,并使概念更难理解,因为有时它们看起来像这样,有时看起来像那样 - 只需查阅这个方便的流程图,看看哪个适用于您!

† 我希望我有一个更好的名字。 已经有结构标签。 我会称它们为标签,但 Go 也有这些标签。 将它们称为字段似乎更合适,也最容易混淆。 如果有人想骑自行车,这个人真的可以用一件新外套。

从某种意义上说,标记联合更类似于结构而不是接口。 它们是一种特殊的结构,一次只能设置一个字段。 从这个角度来看,您的 Foo/Bar 示例就像这样说:

type Foo struct {
  A string
  B int
}

func Bar(f Foo) {...}

func main() {
  Bar("hello world") //same as Bar(Foo{A: "hello world", B: 0})
  Bar(1) //same as Bar(Foo{A: "", B: 1})
}

虽然在这种情况下它是明确的,但我认为这不是一个好主意。

同样在提案中,如果您真的想保存击键,则在明确的情况下允许Bar(Foo{1}) 。 您还可以使用指向联合的指针,因此&Foo{"hello world"}仍然需要复合文字语法。

也就是说,联合确实与接口有相似之处,因为它们具有当前设置了“字段”的动态标签。

switch v := u.[type] {...很好地反映了接口的switch v := i.(type) {... ,同时仍然允许直接在联合值上进行类型切换和断言。 也许它应该是u.[union]以使其更容易被发现,但是无论哪种方式,语法都不那么繁重,并且很清楚它的含义。

您可以提出相同的论点,即.(type)是不必要的,但是当您看到您总是确切地知道发生了什么并且完全证明它是正确的时,在我看来。

这就是我做出这些选择的理由。

@jimmyfrasche
即使在您的解释之后, switch 语法对我来说似乎有点违反直觉。 使用接口, switch v := i.(type) {...在可能的类型之间切换,如切换案例所列,并由.(type)指示。
但是,对于联合,开关不是在可能的类型之间切换,而是在值之间切换。 每个 case 代表一个不同的可能值,其中值实际上可能共享相同的类型。 这更类似于字符串和 int 开关,其中 case 也列出值,并且它们的语法是简单的switch v := u {... 。 因此,对我来说,切换联合的值似乎更自然switch v := u { ... ,因为情况类似,但比整数和字符串的情况更具限制性。

@urandom这是关于语法的一个很好的观点。 事实是,它是我之前没有标签的提案的延续,所以它是当时的类型。 我只是不假思索地盲目复制。 谢谢你指出。

switch u {...可以工作,但switch v := u {...是它看起来太像switch v := f(); v {... (这会使错误报告更加困难——不清楚是哪个意图)。

如果union关键字按照@as 的建议重命名为pick ,那么标签开关可以写为switch u.[pick] {...switch v := u.[pick] {...以保持对称性带有类型开关,但没有混淆,看起来很不错。

即使实现是在 int 上切换,仍然隐式地将 pick 解构为动态标签和存储值,我认为这应该是明确的,无论语法规则如何

您知道,只需调用标签字段并将其设置为字段断言和字段切换就非常有意义。

编辑:虽然这会使使用反射与选择尴尬

[抱歉延迟回复 - 我去度假了]

@ianlancetaylor写道:

我在你的提案中没有看到的是我们为什么要这样做。 添加一种新类型有一个明显的缺点:这是一个新概念,每个学习 Go 的人都必须学习。 什么是补偿优势? 特别是,新的类型给了我们什么,而我们不能从接口类型中得到什么?

我认为有两个主要优点。 首先是语言优势; 二是性能优势。

  • 在处理消息时,尤其是从并发进程中读取时,能够知道可以接收的完整消息集非常有用,因为每条消息都可能带有相关的协议要求。 对于给定的协议,可能的消息类型的数量可能非常少,但是当我们使用开放式接口来表示消息时,该不变量并不清楚。 通常人们会为每种消息类型使用不同的渠道来避免这种情况,但这会带来自己的成本。

  • 有时存在少量已知的可能消息类型,其中没有一个包含指针。 如果我们使用开放式接口来表示它们,我们需要进行分配以生成接口值。 使用限制可能的消息类型的类型意味着可以避免,从而减轻 GC 压力并增加缓存局部性。

对我来说,和类型可以解决的一个特别痛苦是 godoc。 以ast.Spec为例: https :

许多包手动列出命名接口类型的可能底层类型,以便用户无需查看代码或依赖名称后缀或前缀即可快速获得想法。

如果语言已经知道所有可能的值,这可以在godoc自动化,就像带有iotas的枚举类型一样。 它们实际上还可以链接到类型,而不仅仅是纯文本。

编辑:另一个例子: https :

@mvdan这是一个很好的、实用的点,可以在没有任何语言更改的情况下改进

抱歉,您指的只是 godoc 页面中其他名称的链接,但仍手动列出它们吗?

对不起,应该更清楚。

我的意思是自动处理实现 godoc 中当前包中定义的接口的类型的功能请求。

(我相信某处有一个功能请求用于链接手动列出的名称,但我目前没有时间去寻找它)。

我不想接管这个(已经很长)的线程,所以我创建了一个单独的问题 - 见上文。

@Merovius我正在这个问题中回复https://github.com/golang/go/issues/19814#issuecomment -298833986,因为 AST 的东西比枚举更适用于和类型。 很抱歉将您拉入不同的问题。

首先,我想重申一下,我不确定和类型是否属于 Go。 我还没有说服自己,他们绝对不属于。 我正在假设他们这样做是为了探索这个想法并看看它们是否合适。 不管怎样,我都愿意被说服。

其次,您在评论中提到了渐进式代码修复。 根据定义,向 sum 类型添加新术语是一项重大更改,与向接口添加新方法或从结构中删除字段相同。 但这是正确和期望的行为。

让我们考虑使用 Node 接口实现的 AST 示例,它添加了一种新的节点。 假设 AST 是在外部项目中定义的,并且您将其导入到项目中的一个包中,该包会执行 AST。

有几种情况:

  1. 您的代码希望遍历每个节点:
    1.1. 您没有默认语句,您的代码默默地不正确
    1.2. 您有一个带有恐慌的默认语句,您的代码在运行时而不是编译时失败(测试无济于事,因为它们只知道您编写测试时存在的节点)
  2. 您的代码仅检查节点类型的子集:
    2.1. 无论如何,这种新的节点不会出现在子集中
    2.1.1. 只要这个新节点从不包含您感兴趣的任何节点,一切都会顺利
    2.1.2. 否则,您的情况就像您的代码希望遍历每个节点一样
    2.2. 如果您知道的话,这种新类型的节点会出现在您感兴趣的子集中。

对于基于接口的 AST,只有 case 2.1.1 可以正常工作。 这与任何事情一样都是巧合。 渐进式代码修复不起作用。 AST 必须提升其版本,而您的代码需要提升其版本。

详尽的 linter 会有所帮助,但由于 linter 无法检查所有接口类型,因此需要以某种方式告知需要检查特定接口。 这要么意味着源代码中的注释或存储库中的某种配置文件。 如果它是源代码中的注释,因为根据定义,AST 是在一个单独的项目中定义的,因此您可以在该项目的支配下标记接口以进行详尽检查。 只有当整个社区都同意并始终使用单一的详尽性 linter 时,这才能在规模上发挥作用。

使用基于总和的 AST,您仍然需要使用版本控制。 在这种情况下,唯一的区别是穷举性 linter 内置于编译器中。

对 2.2 都没有帮助,但有什么帮助呢?

有一种更简单的、与 AST 相邻的情况,其中和类型会很有用:令牌。 假设您正在为更简单的计算器编写词法分析器。 像*这样的标记没有任何关联的值,像Var这样的标记有一个表示名称的字符串,还有像Val这样的标记保存一个 float64 .

你可以用接口来实现它,但这会很烦人。 不过,你可能会做这样的事情:

package token
type Type int
const (
  Times Type = iota
  // ...
  Var
  Val
)
type Value struct {
  Type
  Name string // only valid if Type == Var
  Number float64 // only valid if Type == Val
}

对基于 iota 的枚举进行详尽的 linter 可以确保永远不会使用非法类型,但对于在 Type == Times 时分配给 Name 或在 Type == Var 时使用 Number 的人来说,它不会工作得很好。 随着代币数量和种类的增加,情况只会变得更糟。 你在这里能做的最好的事情就是添加一个方法Valid() error ,它检查所有的约束和一堆解释你什么时候可以做什么的文档。

sum 类型可以轻松地对所有这些约束进行编码,并且定义将是所有需要的文档。 添加一种新的令牌将是一个突破性的变化,但我所说的关于 AST 的一切仍然适用于这里。

我认为需要更多的工具。 我只是不相信它就足够了。

@jimmyfrasche

其次,您在评论中提到了渐进式代码修复。 根据定义,向 sum 类型添加新术语是一项重大更改,与向接口添加新方法或从结构中删除字段相同。

不,它不一样。 您可以在逐步修复模型中进行这两项更改(对于接口:1. 向所有实现添加新方法,2. 向接口添加方法。对于结构字段:1. 删除所有字段的使用,2. 删除字段)。 在添加一笔类型可以逐步修复模型不能正常工作的情况; 如果你先添加它做 lib,它会破坏所有用户,因为他们不再彻底检查,但你不能先将它添加到用户,因为新案例尚不存在。 删除也是一样。

这不是关于它是否是一个破坏性的变化,而是关于它是否是一个可以在最少中断的情况下精心编排的破坏性变化。

但这是正确和期望的行为。

确切地。 Sum 类型,根据它们的定义和人们想要它们的每一个原因,从根本上与渐进式代码修复的想法不相容。

对于基于接口的 AST,只有 case 2.1.1 可以正常工作。

不,它在 1.2 的情况下也能正常工作(由于无法识别的语法在运行时失败是完全没问题的。不过,我可能不想惊慌,但只是返回一个错误),而且在 2.1 的很多情况下也是如此。 剩下的就是升级软件的基本问题; 如果向库添加新功能,则库的用户需要更改代码才能使用它。 但是,这并不意味着您的软件不正确,直到它

AST 必须提升其版本,而您的代码需要提升其版本。

我完全不明白你所说的如何。 对我来说,说“这个新语法还不能用于所有工具,但它对编译器可用”很好。 就像“如果你在这个新语法上运行这个工具,它会在运行时失败”一样。 在最糟糕的情况下,这只会为逐步修复过程增加另一个步骤:a) 将新节点添加到 AST 包和解析器中。 b) 使用 AST 包修复工具以利用新节点。 c) 更新代码以使用新节点。 是的,新节点只有在 a) 和 b) 完成后才可用; 但是在此过程的每一步中,没有任何损坏,一切仍将编译并正常工作。

我并不是说在渐进式代码修复和没有详尽的编译器检查的世界中你会自动好起来。 它仍然需要仔细规划和执行,您仍然可能会破坏未维护的反向依赖关系,并且可能仍然存在您可能根本无法进行的更改(尽管我想不出任何更改)。 但至少 a) 有一个渐进的升级路径和 b) 这是否应该在运行时破坏您的工具的决定,取决于该工具的作者。 他们可以决定在未知情况下该怎么做。

详尽的 linter 会有所帮助,但由于 linter 无法检查所有接口类型,因此需要以某种方式告知需要检查特定接口。

为什么? 我认为 switchlint™ 可以抱怨任何没有默认情况的类型切换; 毕竟,您希望代码可以与任何接口定义一起使用,因此没有代码来处理未知的实现可能无论如何都是一个问题。 是的,此规则有例外,但已经可以手动忽略例外。

我可能更愿意在编译器中强制执行“每个类型切换都应该需要一个默认情况,即使它是空的”,而不是实际的总和类型。 它既能使迫使人们做出的当面对一个未知的选择他们的代码应该做的决定。

你可以用接口来实现它,但这会很烦人。

耸耸肩,在很少出现的情况下,这是一次性的努力。 对我来说似乎很好。

FWIW,我目前只反对和类型的详尽检查概念。 我还没有对说“任何这些结构定义的类型”的额外便利有任何强烈的看法。

@Merovius我将不得不进一步思考您关于逐步代码修复的优秀观点。 同时:

详尽检查

我目前只反对和类型的详尽检查概念。

您可以使用默认情况明确选择退出详尽性检查(好吧,实际上:默认情况下通过添加涵盖“其他任何事情,无论可能是什么”的情况来使其详尽无遗)。 您仍然可以选择,但您必须明确做出选择。

我认为 switchlint™ 可以抱怨任何没有默认情况的类型开关; 毕竟,您希望代码可以与任何接口定义一起使用,因此没有代码来处理未知的实现可能无论如何都是一个问题。 是的,此规则有例外,但已经可以手动忽略例外。

这是一个有趣的想法。 虽然它会命中用 interface 模拟的 sum 类型和用 const/iota 模拟的枚举,但它并没有告诉你你错过了一个已知的案例,只是你没有处理未知的案例。 无论如何,它似乎很吵。 考虑:

switch {
case n < 0:
case n == 0:
case n > 0:
}

如果 n 是整数(对于浮点数它缺少n != n ),那是详尽无遗的,但是如果没有编码大量有关类型的信息,则可能更容易将其标记为缺少默认值。 对于类似的事情:

switch {
case p[0](a, b):
case p[1](a, b):
//...
case p[N](a, b):
}

即使p[i]ab的类型上形成等价关系,它也无法证明这一点,因此它必须将开关标记为缺少默认值case,这意味着一种通过清单、源代码中的注释、将egrep -v排除在白名单之外的包装脚本或交换机上不必要的默认值(错误地暗示p[i]并不详尽。

无论如何,如果采用“总是抱怨在所有情况下都没有默认值”路线,那么实施短绒棉将是微不足道的。 这样做并在 go-corpus 上运行它会很有趣,看看它在实践中有多嘈杂和/或有用。

令牌

替代令牌实现:

//Type defined as before
type SimpleToken { Type }
type StringToken { Type; Value string }
type NumberToken { Type; Value float64 }
type Interface interface {
  //some method common to all these types, maybe just token() Interface
}

这消除了定义非法令牌状态的可能性,其中某些内容具有字符串和数字值,但不允许创建StringToken的类型应该是SimpleToken或副反之。

要使用接口做到这一点,您需要为每个标记定义一种类型( type Plus struct{}type Mul struct{}等),并且大多数定义与类型名称完全相同。 一次或不一次的工作量很大(尽管在这种情况下非常适合代码生成)。

我想你可以有一个令牌接口的“层次结构”来根据允许的值来划分令牌的种类:(假设在这个例子中,有不止一种令牌可以包含数字或字符串等)

type SimpleToken int //implements token.Interface
const (
  Plus SimpleToken = iota
  // ...
}
type NumericToken interface {
  Interface
  Value() float64
  nt() NumericToken
}
type IntToken struct { //implements NumericToken, and a FloatToken
type StringToken interface { // for Var and Func and Const, etc.
  Interface
  Value() string
  st() StringToken
}

无论如何,这意味着每个标记都需要一个指针引用来访问其值,这与 struct 或 sum 类型不同,后者仅在涉及字符串时才需要指针。 因此,通过适当的 linters 和对 godoc 的改进,在这种情况下 sum 类型的巨大胜利与最小化分配有关,同时禁止非法状态和输入量(在键盘意义上),这似乎并不重要。

您可以使用默认情况明确选择退出详尽性检查(好吧,实际上:默认情况下通过添加涵盖“其他任何事情,无论可能是什么”的情况来使其详尽无遗)。 您仍然可以选择,但您必须明确做出选择。

因此,无论哪种方式,我们都可以选择加入或退出详尽的检查:)

它不会告诉你你错过了一个已知的案例,只是你没有处理未知的案例。

实际上,我相信编译器已经进行了整个程序的分析,以确定我认为在哪些接口中使用了哪些具体类型? 我至少希望它,至少对于非接口类型断言(即,不是对接口类型断言,而是对具体类型进行断言的类型断言),在编译时生成接口中使用的函数表。
但是,老实说,这是从第一原则出发的,我对实际实施一无所知。

在任何情况下,都应该很容易,a) 列出在整个程序中定义的任何具体类型,b) 对于任何类型切换,根据它们是否实现该接口来过滤它们。 如果你使用像这样,你会最终有一个可靠的名单。 我认为。

我不是 100% 相信可以编写一个与实际明确说明选项一样可靠的工具,但我相信您可以涵盖 90% 的情况,并且您绝对可以编写一个工具来执行此操作之外编译器,给出正确的注释(即使 sum-types 成为类似 pragma 的注释,而不是实际类型)。 诚然,这不是一个很好的解决方案。

无论如何,它似乎很吵。 考虑:

我认为这是不公平的。 您提到的案例与 sum-types 完全没有关系。 如果我在哪里编写这样的工具,我会将它限制为类型开关和带有表达式的开关,因为这些似乎也是处理 sum 类型的方式。

替代令牌实现:

为什么不是标记方法? 您不需要类型字段,您可以从接口表示中免费获得它。 如果您担心一遍又一遍地重复标记方法; 定义一个未导出的 struct{},给它那个标记方法并将其嵌入到每个实现中,与您的方法相比,每个选项的额外成本为零且输入更少。

无论如何,这意味着每个令牌都需要一个指针来访问它的值

是的。 这是一个真正的成本,但我认为它基本上不会超过任何其他论点。

我认为这是不公平的。

确实如此。

我写了一个快速而肮脏的版本并在 stdlib 上运行它。 检查任何switch 语句有 1956 次命中,限制它跳过switch {形式将计数减少到 1677。我没有检查任何这些位置,看看结果是否有意义。

https://github.com/jimmyfrasche/switchlint

当然还有很大的改进空间。 它不是非常复杂。 欢迎拉取请求。

(其余的我稍后回复)

编辑:错误的标记格式

我认为这是迄今为止所有内容的(相当有偏见的)总结(并且自恋地假设我的第二个提案)

优点

  • 简洁,易于以自记录的方式简洁地编写许多约束
  • 更好地控制分配
  • 更容易优化(编译器已知的所有可能性)
  • 详尽检查(如果需要,可以选择退出)

缺点

  • 对 sum 类型成员的任何更改都是破坏性更改,除非所有外部包都选择退出详尽检查,否则不允许逐步修复代码
  • 语言中的另一件事要学习,与现有功能的一些概念重叠
  • 垃圾收集器必须知道哪些成员是指针
  • 对于1 + 1 + ⋯ + 1形式的总和很尴尬

备择方案

  • iota "enum" 用于1 + 1 + ⋯ + 1形式的总和
  • 与未导出的标记方法接口,用于更复杂的总和(可能生成)
  • 或具有 iota 枚举和关于根据枚举值设置哪些字段的额外语言规则的结构

不管

  • 更好的工具,总是更好的工具

对于逐步修复,这是一个很大的问题,我认为唯一的选择是外部包选择退出详尽检查。 这确实意味着,即使您以其他方式匹配其他所有内容,但仅与未来证明有关的“不必要的”默认情况必须是合法的。 我相信现在这是隐含的真实,如果还不够容易指定的话。

包维护者可能会发布声明“嘿,我们将在下一个版本中向此 sum 类型添加一个新成员,请确保您可以处理它”,然后 switchlint 工具可以找到任何需要的情况被选择退出。

不像其他情况那么简单,但仍然很可行。

在编写使用外部定义的和类型的程序时,您可以注释掉默认值以确保您没有遗漏任何已知情况,然后在提交之前取消注释。 或者可能有一个工具让你知道默认值是“不必要的”,它告诉你你知道一切都知道,并且可以防止未知的未来。

假设我们希望在使用模拟和类型的接口类型时选择使用 linter 进行详尽检查,而不管它们是在哪个包中定义的。

@Merovius您的betterSumType() BetterSumType技巧非常酷,但这意味着必须在定义包中进行切换(或者您公开类似

func CallBeforeSwitches(b BetterSumType) (BetterSumType, bool) {
    if b == nil {
        return nil, false
    }
    b = b.betterSumType()
    if b == nil {
        return nil, false
    }
    return b, true
}

以及每次都调用的 lint)。

检查程序中的所有开关是否详尽无遗的必要标准是什么?

它不能是空的界面,因为那是什么游戏。 所以它至少需要一种方法。

如果接口没有未导出的方法,则任何类型都可以实现它,因此详尽程度将取决于每个开关的调用图上的所有包。 可以导入一个包,实现它的接口,然后将该值发送到包的功能之一; 因此,如果不创建导入周期,该函数中的切换将无法详尽无遗。 所以它至少需要一种未导出的方法。 (这包含了先前的标准)。

嵌入会弄乱我们正在寻找的属性,因此我们需要确保包的任何导入器都不会在任何时候嵌入接口或任何实现它的类型。 如果我们从不调用创建嵌入值的某个函数,或者没有任何嵌入的接口“逃离”包的 API 边界,那么一个真正奇特的 linter 可能会告诉我们有时嵌入是可以的。

为了彻底,我们要么需要检查接口的零值从不传递,要么强制执行详尽的开关检查case nil 。 (后者更容易,但前者更受欢迎,因为包含 nil 会将“A 型或 B 型或 C 型”和变成“无或 A 型或 B 型或 C 型”和)。

假设我们有一个 linter,具有所有这些能力,甚至是可选的能力,可以验证任何导入树和该树中任何给定接口的语义。

现在假设我们有一个依赖项 D 的项目。我们希望确保在我们的项目中 D 的一个包中定义的接口是详尽的。 让我们说它确实如此。

现在,我们需要为我们的项目 D' 添加一个新的依赖项。 如果 D' 导入定义了相关接口类型的 D 中的包但不使用这个 linter,它可以很容易地破坏我们使用穷举开关需要保持的不变量。

就此而言,假设 D 只是巧合地通过了 linter,而不是因为维护者运行它。 升级到 D 可以像 D' 一样轻松地破坏不变量。

即使 linter 可以说“现在这是 100% 详尽的 👍”,但我们无需做任何事情就可以改变。

“iota enums”的详尽检查器似乎更容易。

对于所有type t u其中u是积分和t用作const与单独指定的值或iota使得零u值包含在这些常量中。

笔记:

  • 重复值可被视为别名并在此分析中被忽略。 我们将假设所有命名常量都有不同的值。
  • 1 << iota可能被视为幂集,我相信至少在大多数情况下,但可能需要额外的条件,尤其是围绕按位补码。 暂时不考虑

对于一些速记,我们将min(t)称为常量,这样对于任何其他常量, Cmin(t) <= C ,并且类似地,我们将max(t)称为常量,例如对于任何其他常量, CC <= max(t)

为了确保t被彻底使用,我们需要确保

  • t的值始终是命名常量(或在某些惯用位置为 0,如函数调用)
  • min(t) <= v <= max(t)之外没有tv值的不等式比较
  • t的值从不用于算术运算+/等。当结果夹在min(t)max(t)之间时可能会出现异常t
  • 开关包含t或默认情况的所有常量。

这仍然需要验证导入树中的所有包,并且可以很容易地失效,尽管在惯用代码中失效的可能性较小。

我的理解是,这类似于类型别名,不会破坏更改,那么为什么要为 Go 2 保留它呢?

类型别名不会引入新的关键字,这是一个明确的突破性变化。 似乎也暂停了即使是微小的语言变化,这将是一个重大变化。 即使只是改造所有编组/解组例程来处理反映的总和值也将是一个巨大的考验。

类型别名正在修复一个没有解决方法的问题。 Sum 类型在类型安全方面提供了好处,但它不是没有它们的表现。

只有一点(次要)支持@rogpeppe的原始提案。 在包http ,有接口类型Handler和实现它的函数类型HandlerFunc 。 现在,为了将函数传递给http.Handle ,您必须明确地将其转换为HandlerFunc 。 如果http.Handle改为接受HandlerFunc | Handler类型的参数,则它可以接受任何可直接分配给HandlerFunc函数/闭包。 联合有效地用作类型提示,告诉编译器如何将具有未命名类型的值转换为接口类型。 由于HandlerFunc实现了Handler ,否则联合类型的行为将与Handler完全相同。

@griesemer回应您在枚举线程中的评论, https: //github.com/golang/go/issues/19814#issuecomment -322752526,我认为我在此线程中早些时候提出的建议https://github.com/golang/ go/issues/19412#issuecomment -289588569 解决了总和类型(“swift 样式枚举”)在 Go 中必须如何工作的问题。 虽然我想他们,我不知道他们是否有必要除了围棋,但我认为,如果他们加入他们不得不看/操作很象。

那个帖子不完整,整个帖子前后都有澄清,但我不介意重申这些观点或总结,因为这个帖子很长。

如果您有一个由带有类型标签的接口模拟的 sum 类型并且绝对不能通过嵌入来规避它,这是我提出的最好的防御: https :

@jimmyfrasche这个

另一种可能的方法是: https :

@rogpeppe如果您要使用反射,为什么不直接使用反射?

我已经根据此处和其他问题中的评论编写了我的第二个提案的修订版。

值得注意的是,我已经删除了详尽性检查。 然而,为下面的提议编写外部详尽检查器是微不足道的,尽管我不相信可以为用于模拟 sum 类型的其他 Go 类型编写一个。

编辑:我已经删除了在选择值的动态值上键入断言的能力。 这太神奇了,代码生成也提供了允许它的原因。

Edit2:阐明了在另一个包中定义选择时字段名称如何与断言和开关一起使用。

Edit3:限制嵌入和澄清隐式字段名称

Edit4:澄清开关中的默认值

选择类型

Pick 是一种在语法上类似于结构的复合类型:

pick {
  A, B S
  C, D T
  E U "a pick tag"
}

上面的ABCDE是选择的字段名, STU分别是这些字段的类型。 字段名称可以导出或不导出。

没有间接性,pick 可能不是递归的。

合法的

type p pick {
    //...
    p *p
}

非法的

type p pick {
    //...
    p p
}

选择没有嵌入,但选择可以嵌入到结构中。 如果选择嵌入在结构中,则选择上的方法将提升为结构,但选择的字段不会。

没有字段名的类型是定义与类型同名的字段的简写。 (这是一个错误,如果该类型是无名,与异常*T其中名称为T )。

例如,

type p pick {
    io.Reader
    io.Writer
    string
}

具有三个字段ReaderWriterstring ,具有各自的类型。 请注意,字段string未导出,即使它在 Universe 范围内。

选择类型的值由动态字段和该字段的值组成。

拾取类型的零值是它在源顺序中的第一个字段和该字段的零值。

给定两个相同选择类型的值ab ,可以将选择值分配为任何其他值

a = b

分配非选择值,即使是选择中的一个字段的一种类型,也是非法的。

一种选择类型在任何给定时间都只有一个动态字段。

复合字面量语法类似于结构体,但有额外的限制。 即,无键文字总是无效的,只能指定一个键。

以下内容有效

pick{A string; B int}{A: "string"} //value is (B, "string")
pick{A, B int}{B: 1} //value is (B, 1)
pick{A, B string}{} //value is (A, "")

以下是编译时错误:

pick{A int; B string}{A: 1, B: "string"} //a pick can only have one value at a time
pick{A int; B uint}{1} //pick composite literals must be keyed

给定一个价值ppick {A int; B string}以下任务

p.B = "hi"

p的动态字段设置为B ,将B为“hi”。

分配给当前动态字段会更新该字段的值。 设置新动态字段的赋值必须将任何未指定的内存位置归零。 对选择字段的选择或结构字段的分配根据需要更新或设置动态字段。

type P pick {
    A, B image.Point
}

var p P
fmt.Println(P) //{A: {0 0}}

p.A.X = 1 //A is the dynamic field, update
fmt.Println(P) //{A: {1 0}}

p.B.Y = 2 //B is not the dynamic value, create zero image.Point first
fmt.Println(P) //{B: {0 2}}

选择中保存的值只能由字段断言或字段开关访问。

x := p.[X] //panics if X is not the dynamic field of p
x, ok := p.[X] //returns the zero value of X and false if X is not the dynamic field of p

switch v := p.[var] {
case A:
case B, C: // v is only defined in this case if fields B and C have identical type names
case D:
default: // always legal even if all fields are exhaustively listed above
}

字段断言和字段开关中的字段名称是类型的属性,而不是定义它的包。它们不是也不能由定义pick的包名称限定。

这是有效的:

_, ok := externalPackage.ReturnsPick().[Field]

这是无效的:

_, ok := externalPackage.ReturnsPick().[externalPackage.Field]

字段断言和字段切换始终返回动态字段值的副本。

未导出的字段名称只能在其定义包中声明。

类型断言和类型切换也适用于选择。

//removed, see note at top
//v, ok := p.(fmt.Stringer) //holds if the type of the dynamic field implements fmt.Stringer
//v, ok := p.(int) //holds if the type of the dynamic field is an int

类型断言和类型开关总是返回动态字段值的副本。

如果pick 存储在接口中,则接口的类型断言仅与pick 本身的方法集匹配。 [仍然正确但多余,因为上述内容已被删除]

如果选择的所有类型都支持相等运算符,则:

  • 该选择的值可以用作地图键
  • 同一个pick的两个值是==如果它们具有相同的动态字段并且其值为==
  • 即使值是==具有不同动态字段的两个值也是!= ==

选择类型的值不支持其他运算符。

一个选择类型P值可以转换为另一个选择类型Q如果P的字段名称及其类型的集合是字段名称及其类型的子集输入Q

如果PQ定义在不同的包中并且具有未导出的字段,则无论名称和类型如何,这些字段都被视为不同。

例子:

type P pick {A int; B string}
type Q pick {B string; A int; C float64}

//legal
var p P
q := Q(p)

//illegal
var q Q
p := P(Q) //cannot handle field C

两种选择类型之间的可分配性被定义为可转换性,只要定义的类型不超过一种。

方法可以在定义的选择类型上声明。

我创建了(并添加到 wiki)一份体验报告https://gist.github.com/jimmyfrasche/ba2b709cdc390585ba8c43c989797325

编辑:和:心脏: @mewmew留下了更好、更详细的报告作为对该要点的答复

如果我们有办法说,对于给定类型T ,可以转换为类型T或分配给类型T的变量的类型列表怎么办? 例如

type T interface{} restrict { string, error }

定义一个名为T的空接口类型,这样可以分配给它的唯一类型是stringerror 。 任何分配任何其他类型值的尝试都会产生编译时错误。 现在我可以说

func FindOrFail(m map[int]string, key int) T {
    if v, ok := m[key]; ok {
        return v
    }
    return errors.New("no such key")
}

func Lookup() {
    v := FindOrFail(m, key)
    if err, ok := v.(error); ok {
        log.Fatal(err)
    }
    s := v.(string) // This type assertion must succeed.
}

这种方法不会满足和类型(或选择类型)的哪些关键元素?

s := v.(string) // This type assertion must succeed.

这并非严格正确,因为v也可以是nil 。 需要对语言进行相当大的更改才能消除这种可能性,因为这意味着引入不具有零值的类型以及所有需要的类型。 零值简化了语言的一部分,但也使设计这些类型的功能更加困难。

有趣的是,这种方法与@rogpeppe的原始提议非常相似。 它没有对列出的类型进行强制,这在我之前指出的情况下可能很有用( http.Handler )。 另一件事是它要求每个变体都是不同的类型,因为变体是通过类型而不是不同的标签来区分的。 我认为这是严格的表现力,但有些人更喜欢有不同的标签和类型是不同的。

@ianlancetaylor

优点

  • 可以限制为一组封闭的类型——这绝对是主要的事情
  • 可以编写一个精确的详尽检查器
  • 你会得到“你可以分配一个满足合同的值”属性。 (我不在乎这个,但我想其他人会这样做)。

缺点

  • 它们只是有好处的接口,并不是真正不同的类型(虽然好处不错!)
  • 你仍然有 nil 所以它不是类型理论意义上的和类型。 你指定的任何A + B + C实际上是一个1 + A + B + C ,你没有选择。 正如@stevenblenkinsop在我研究这个问题时指出的那样。
  • 更重要的是,由于那个隐式指针,你总是有一个间接的。 通过选择建议,您可以选择p*p让您更好地控制内存权衡。 您不能将它们实现为可区分联合(在 C 意义上)作为优化。
  • 没有零值的选择,这是一个非常好的属性,尤其是因为在 Go 中拥有尽可能有用的零值非常重要
  • 大概你不能在T上定义方法(但大概你有restrict 修改的接口的方法,但restrict 中的类型需要满足它?否则我看不到重点不只是有type T restrict {string, error} )
  • 如果您丢失了字段/summands/what-have-you 的标签,那么当它与接口类型交互时会变得混乱。 你失去了和类型的强大的“正是这个或正是那个”属性。 您可以放入io.Reader并取出io.Writer 。 这对(不受限制的)接口有意义,但对 sum 类型没有意义。
  • 如果你想让两个相同的类型意味着不同的东西,你需要使用包装器类型来消除歧义; 这样的标签必须在外部命名空间中,而不是像结构字段那样局限于类型
  • 这可能对您的具体措辞读得太多了,但听起来它根据受让人的类型改变了可分配性规则(我读它是说您不能将可分配的东西分配给errorT必须完全是一个错误)。

也就是说,它确实检查了主要框(我列出的前两个专业人士),如果这是我所能得到的,我会立即接受。 不过,我希望更好。

我假设应用了类型断言规则。 因此类型需要与具体类型相同或可分配给接口类型。 基本上,它的工作原理与接口完全一样,但任何值(除了nil )都必须至少对列出的一种类型进行断言。

@jimmyfrasche
在您更新的提案中,如果该类型的所有元素都是不同类型,是否可以进行以下分配:

type p pick {
    A int
    B string
}

func Foo(P p) {
}

var P p = 42
var Q p = "foo"

Foo(42)
Foo("foo")

当此类分配可能时,和类型的可用性要大得多。

通过选择建议,您可以选择p*p让您更好地控制内存权衡。

接口分配存储标量值的原因是您不必读取类型字来确定另一个字是否是指针; 见#8405 讨论。 相同的实现考虑可能适用于选择类型,这可能意味着在实践中p最终会分配并且无论如何都是非本地的。

@urandom不,鉴于您的定义,必须编写它

var p P = P{A: 42} // p := P{A: 42}
var q P = P{B: "foo")
Foo(P{A: 42}) // or Foo({A: 42}) if types can be elided here
Foo(P{B: "foo"})

最好将它们视为一次只能设置一个字段的结构。

如果你没有那个,然后你添加一个C uintp会发生什么p = 42

您可以根据顺序和可分配性制定许多规则,但它们总是意味着对类型定义的更改会对使用该类型的所有代码产生微妙而显着的影响。

在最好的情况下,由于没有歧义,更改会破坏所有代码,并表示您需要在再次编译之前将其更改为p = int(42)p = uint(42) 。 一行更改不应该需要修复一百行。 特别是如果这些行根据您的代码在人员包中。

你要么必须 100% 明确,要么有一种非常脆弱的类型,没有人可以触及,因为它可能会破坏一切。

这适用于任何总和类型提案,但如果有明确的标签,您仍然具有可分配性,因为标签明确表示要分配给哪种类型。

@josharian因此,如果我正确地阅读了这一点,那么 iface 现在总是(*type, *value)而不是像 Go 之前那样在第二个字段中存储字大小的值,因此并发 GC 不需要检查两者字段来查看第二个是否是一个指针——它可以假设它总是。 我做对了吗?

换句话说,如果选择类型被实现(使用 C 符号)像

struct {
    int which;
    union {
         A a;
         B b;
         C c;
    } summands;
}

GC 需要锁定(或其他花哨但等效的东西)来检查which以确定是否需要扫描summands

iface 现在总是 (*type, *value) 而不是像 Go 之前那样在第二个字段中存储字大小的值的原因是并发 GC 不需要检查两个字段来查看第二个字段是否是指针——它可以假设它总是如此。

这是正确的。

当然,pick 类型的有限性质将允许一些替代实现。 选择类型的布局可以使指针/非指针始终具有一致的模式; 例如,所有标量类型都可以重叠,并且字符串字段可以与切片字段的开头重叠(因为两者都以“指针,非指针”开头)。 所以

pick {
  a uintptr
  b string
  c []byte
}

可以大致布局:

[ word 1 (ptr) ] [ word 2 (non-ptr) ] [ word 3 (non-ptr) ]
[    <nil>         ] [                 a           ] [                              ]
[       b.ptr      ] [            b.len          ] [                              ]
[       c.ptr      ] [             c.len         ] [        c.cap             ]

但其他拾取类型可能不允许这种最佳包装。 (对于损坏的 ASCII 感到抱歉,我似乎无法让 GitHub 正确呈现它。我希望你明白这一点。)

这种进行静态布局的能力甚至可能是支持包含选择类型的性能参数; 我在这里的目标只是为您标记相关的实现细节。

@josharian并感谢您这样做。 我没有想到这一点(老实说,我只是在谷歌上搜索是否存在关于如何 GC 歧视联合的研究,看到是的,你可以这样做并称之为一天 - 由于某种原因,我的大脑没有将“并发”联系起来那天“去”:facepalm!)。

如果其中一种类型是已经具有布局的已定义结构,则选择会更少。

一种选择是不“压缩”被加数,如果它们包含指针意味着大小将与等效结构相同(对于鉴别器 int 为 + 1)。 在可能的情况下,也许采用混合方法,以便所有可以共享布局的类型都这样做。

失去漂亮的大小属性将是一种耻辱,但这实际上只是一种优化。

即使它总是1 + 等效结构的大小,即使它们不包含指针,它仍然具有该类型本身的所有其他良好属性,包括对分配的控制。 随着时间的推移,可以添加其他优化,并且至少如您所指出的那样是可能的。

type p pick {
    A int
    B string
}

A 和 B 需要在那里吗? 一个选择从一组类型中挑选,那么为什么不完全丢弃它们的标识符名称:

type p pick {
    int
    string
}
q := p{string: "hello"}

我相信这种形式已经对 struct 有效。 可能有一个约束,它需要选择。

@as如果字段名称被省略,它与类型相同,因此您的示例可以工作,但是由于这些字段名称未导出,因此只能从定义包中设置/访问它们。

字段名称确实需要在那里,即使是基于类型名称隐式生成的,或者与可分配性和接口类型存在不良交互。 字段名称使其与 Go 的其余部分一起工作。

@as道歉,我刚刚意识到你的意思与我读到的不同。

您的公式有效,但是您有一些看起来像 struct 字段但由于通常导出/未导出的东西而表现不同的东西。

是否可以从定义p的包外部访问字符串,因为它在宇宙中?

关于什么

type t struct {}
type P pick {
  t
  //other stuff
}

?

通过将字段名称与类型名称分开,您可以执行以下操作

pick {
  unexported Exported
  Exported unexported
}

甚至

pick { Recoverable, Fatal error }

如果选择字段的行为类似于结构字段,您可以使用很多您已经了解的关于结构字段的知识来考虑选择字段。 唯一真正的区别是一次只能选择一个可以设置的字段。

@jimmyfrasche
Go 已经支持在结构体中嵌入匿名类型,因此范围的限制是语言中已经存在的限制,我相信这个问题正在通过类型别名来解决。 但承认我还没有想到所有可能的用例。 这似乎取决于这个习语在 Go 中是否常见:

package p
type T struct{
    Exported t
}
type t struct{}

小 _t_ 存在于一个包中,它嵌入在大T 中,它唯一的暴露是通过这样的导出类型。

@作为

但是,我不确定我是否完全遵循:

//with the option to have field names
pick { //T is in the namespace of the pick and the type isn't exposed to other packages
  T t
  //...
}

//without
type T = t //T has to be defined in the outer scope and now t is exposed to other packages
pick {
  T
  //...
}

此外,如果您只有标签的类型名称,要包含[]string您需要执行type Strings = []string

这正是我希望看到选择类型实现的方式。 在
特别是 Rust 和 C++(性能的黄金标准)是如何做的
它。

如果我只是想进行详尽检查,我可以使用检查器。 我想要
性能取胜。 这意味着选择类型也不能为零。

不应该允许获取一个 pick 元素的成员的地址(它
不是内存安全的,即使在单线程的情况下,正如众所周知的
Rust 社区。)。 如果这需要对拾取类型进行其他限制,
那么就这样吧。 但对我来说,选择类型总是在堆上分配
会很糟糕。

2017 年 8 月 18 日下午 12:01,“jimmyfrasche”通知@github.com 写道:

@josharian https://github.com/josharian所以如果我读对了
iface 现在总是 (*type, *value) 而不是 stash 的原因
Go 之前所做的第二个字段中的字大小值是这样的
并发 GC 不需要检查两个字段以查看第二个字段是否
是一个指针——它可以假设它总是如此。 我做对了吗?

换句话说,如果选择类型被实现(使用 C 符号)像

结构{
诠释其中;
联合{
一个;
乙乙;
Cc;
} 求和;
}

GC 需要锁定(或一些花哨但等效的东西)以
检查哪个以确定是否需要扫描被加数?


您收到此消息是因为您创作了该线程。
直接回复本邮件,在GitHub上查看
https://github.com/golang/go/issues/19412#issuecomment-323393003或静音
线程
https://github.com/notifications/unsubscribe-auth/AGGWB3Ayi31dYwotewcfgmCQL-XVrfxIks5sZbVrgaJpZM4MTmSr
.

@黛米玛丽

不应允许获取 pick 元素成员的地址(这不是内存安全的,即使在单线程情况下也是如此,这在 Rust 社区中是众所周知的。)。 如果这需要对选择类型进行其他限制,那么就这样吧。

那是个很好的观点。 我有那个,但它一定是在编辑中丢失了。 我确实包括,当你从一个选择访问值时,它总是出于同样的原因返回一个副本。

作为为什么这是真的例子,为了后代,请考虑

v := pick{ A int; B bool }{A: 5}
p := &v.[A] //(this would be illegal but pretending it's not for a second)
v.B = true

如果v被优化以便字段AB在内存中占据相同的位置,那么p不是指向 int:它指向一个布尔值。 违反了内存安全。

@jimmyfrasche

您不希望内容可寻址的第二个原因是变异语义。 如果该在某些情况下间接地存储,然后

v := pick{ A int; ... }{A: 5}
v2 := v

v2.[A] = 6 // (this would be illegal under the proposal, but would be 
           // permitted if `v2.[A]` were addressable)

fmt.Println(v.[A]) // would print 6 if the contents of the pick are stored indirectly

pick与接口相似的一个地方是,如果在其中存储值,您希望保留值语义。 如果您可能需要间接作为实现细节,唯一的选择是使内容不可寻址(或者更准确地说,可变寻址,但目前在 Go 中不存在区别),这样您就无法观察到别名.

编辑:哎呀(见下文)

@jimmyfrasche

拾取类型的零值是它在源顺序中的第一个字段和该字段的零值。

请注意,如果第一个字段需要间接存储,这将不起作用,除非您将零值特殊情况下,以便v.[A]v.(error)做正确的事情。

@stevenblenkinsop我不确定“第一个字段需要间接存储”是什么意思。 我假设您的意思是第一个字段是指针还是隐式包含指针的类型。 如果是这样,下面有一个例子。 如果不是,你能澄清一下吗?

给定的

var p pick { A error; B int }

零值p具有动态字段A并且A值为 nil。

我指的不是pick中存储的值是/包含指针,而是指由于垃圾收集器施加的布局限制而间接存储的非指针值,如@josharian 所述.

在您的示例中, p.B - 不是指针 - 将无法与p.A共享重叠存储,其中包含两个指针。 它很可能必须间接存储(即表示为*int ,当您访问它时会自动取消引用,而不是int )。 如果p.B是第一个字段,则pick的零值将是new(int) ,这不是可接受的零值,因为它需要初始化。 您需要对其进行特殊处理,以便将 nil *int视为new(int)

@jimmyfrasche
哦对不起。 回顾对话,我意识到您正在考虑使用相邻存储来存储布局不兼容的变体,而不是复制非指针类型的间接存储的接口机制。 在这种情况下,我的最后三个评论没有意义。

编辑:哎呀,比赛条件。 发帖然后看到你的评论。

@stevenblenkinsop啊,好吧,我明白你的意思。 但这不是问题。

共享重叠存储是一种优化。 它永远不会这样做:类型的语义是重要的一点。

如果编译器可以优化存储并选择这样做,这是一个不错的奖励。

在您的示例中,编译器可以完全按照等效结构存储它(添加标签以了解哪个是活动字段)。 这将是

struct {
  which_field int // 0 = A, 1 = B
  A error
  B int
}

零值仍然是所有字节 0 并且没有必要秘密分配作为特殊情况。

重要的部分是确保在给定时间只有一个领域在起作用。

允许在选择上进行类型断言/切换的动机是,例如,如果选择中的每个类型都满足fmt.Stringer您可以在选择上编写一个方法,例如

func (p P) String() string {
  return p.(fmt.Stringer).String()
}

但是由于选择字段的类型可以是接口,这会产生微妙的影响。

如果前面示例中的选择P有一个本身类型为fmt.Stringer的字段,那么String方法会在该字段是动态字段且其值为nil发生恐慌nil接口,甚至不是它本身。 https://play.golang.org/p/HMYglwyVbl虽然这一直是正确的,但它只是不定期出现,但它可以更经常地出现。

然而,和类型的封闭性质将允许详尽的 linter 找到可能出现的任何地方(可能会出现一些误报)并报告需要处理的情况。

同样令人惊讶的是,如果您可以在选择上实现方法,那么这些方法不用于满足类型断言。

type Num pick { A int; B float32 }

func (n Num) String() string {
      switch v := n.[var] {
      case A:
          return fmt.Sprint(v)
      case B:
          return fmt.Sprint(v)
      }
}
...
n := Num{A: 5}
s1, ok := p.(fmt.Stringer) // ok == false
var i interface{} = p
s2, ok := i.(fmt.Stringer) // ok == true

如果它们满足接口,您可以让类型断言从当前字段提升方法,但这会遇到它自己的问题,例如是否从接口本身未定义的接口字段中的值提升方法(或甚至如何有效地实现这一点)。 此外,人们可能会期望将所有字段通用的方法提升到选择本身,但是它们必须在每次调用时通过变体选择进行调度,此外,如果选择存储在接口中,则还可能进行虚拟调度,和/或如果该字段是一个接口,则为虚拟分派。

编辑:顺便说一句,最佳打包一个pick是最短常见超弦问题的一个实例,它是NP完全的,尽管有常用的贪婪近似。

规则是如果它是一个选择值,则类型断言在选择值的动态字段上断言,但如果选择值存储在接口中,则类型断言在选择类型的方法集上。 起初可能会令人惊讶,但它相当一致。

仅删除允许对选择值的类型断言不会有问题。 不过这将是一种耻辱,因为它确实可以很容易地提升选择中所有类型共享的方法,而无需写出所有案例或使用反射。

尽管如此,使用代码生成来编写

func (p Pick) String() string {
  switch v := p.[var] {
  case A:
    return v.String()
  case B:
    return v.String()
  //etc
  }
}

只是继续并删除类型断言。 也许应该添加它们,但它们不是提案的必要部分。

我想回到@ianlancetaylor之前的评论,因为在对错误处理进行了更多思考后,我对它有了一些新的看法(具体来说,https://github.com/golang/go/issues/21161#问题评论-320294933)。

特别是,新的类型给了我们什么,而我们不能从接口类型中得到什么?

在我看来,sum-types 的主要优点是它们允许我们区分返回多个值和返回多个值之一——特别是当这些值之一是错误接口的实例时。

我们目前有很多形式的功能

func F(…) (T, error) {
    …
}

它们中的一些,如io.Reader.Readio.Reader.Write ,返回一个T与沿error ,而其他返回一个Terror但永远不会两者兼而有之。 对于前一种风格的 API,在出错的情况下忽略T通常是一个错误(例如,如果错误是io.EOF ); 对于后一种样式,返回非零T是错误。

自动化工具,包括lint ,可以检查特定函数的使用,以确保当错误非零时该值被(或不被)正确忽略,但这种检查自然不会扩展到任意函数。

例如,如果错误是RequiredNotSetError ,则proto.Marshal旨在成为“值错误”样式,否则似乎是“值错误”样式。 因为类型系统不区分两者,所以很容易不小心引入回归:要么在应该返回值时不返回值,要么在不应该返回值时返回值。 而proto.Marshaler使问题进一步复杂化。

另一方面,如果我们可以将类型表示为联合,我们可以更明确地说明它:

type PartialMarshal struct {
    Data []byte // The marshalled value, ignoring unset required fields.
    MissingFields []string
}

func Marshal(pb Message) []byte | PartialMarshal | error

@ianlancetaylor ,我一直在研究你的纸上提案。 如果以下内容不正确,您能告诉我吗?

给定的

var r interface{} restrict { uint, int } = 1

r的动态类型是int ,并且

var _ interface{} restrict { uint32, int32 } = 1

是非法的。

给定的

type R interface{} restrict { struct { n int }, etc }
type S struct { n int }

那么var _ R = S{}将是非法的。

但鉴于

type R interface{} restrict { int, error }
type A interface {
  error
  foo()
}
type C struct { error }
func (C) foo() {}

var _ R = C{}var _ R = A(C{})都是合法的。

两个都

interface{} restrict { io.Reader, io.Writer }

interface{} restrict { io.Reader, io.Writer, *bytes.Buffer }

是等价的。

同样地,

interface{} restrict { error, net.Error }

相当于

interface { Error() string }

给定的

type IO interface{} restrict { io.Reader, io.Writer }
type R interface{} restrict {
  interface{} restrict { int, uint },
  IO,
}

那么R的底层类型等价于

interface{} restrict { io.Writer, uint, io.Reader, int }

编辑:斜体小更正

@jimmyfrasche我不会说我上面写的是一个提案。 这更像是一个想法。 我将不得不考虑您的评论,但乍一看它们似乎是合理的。

@jimmyfrasche的提议几乎是我直觉上期望选择类型在 Go 中表现的方式。 我认为特别值得注意的是,他建议使用第一个字段的零值作为选择的零值是直观的“零值意味着将字节清零”,前提是标签值从零开始(也许这已经注意到;这个线程现在很长......)。 我也喜欢性能影响(没有不必要的分配),并且选择与接口完全正交(在包含接口的选择上切换没有令人惊讶的行为)。

我唯一会考虑改变的是改变标签: foo.X = 0似乎可能是foo = Foo{X: 0} ; 还有几个字符,但更明确的是它正在重置标签并将值归零。 这是一个小问题,如果他的提议按原样被接受,我仍然会很高兴。

@ns-cweber 谢谢,但我不能相信零值行为。 这些想法已经流传了一段时间,并且出现在

至于foo.X = 0foo = Foo{X: 0} ,我的提议实际上允许两者。 如果选择的该字段是结构,则后者很有用,因此您可以执行foo.X.Y = 0而不是foo = Foo{X: image.Point{X: foo.[X].X, 0}} ,这除了冗长之外还可能在运行时失败。

我还认为保持原样会有所帮助,因为它强化了电梯语义的语义:它是一个结构,一次只能设置一个字段。

可能阻止它被原样接受的一件事是在结构中嵌入一个选择将如何工作。 前几天我意识到我掩盖了使用结构体的各种影响。 我认为它是可以修复的,但不完全确定最好的修复方法是什么。 最简单的方法是它只继承方法,您必须通过名称直接引用嵌入的 pick 以获取其字段,我倾向于这样做以避免结构同时具有 struct 字段和 pick 字段。

@jimmyfrasche感谢您纠正我关于零值行为的问题。 我同意你的提议允许两个变异者,我认为你的电梯推销点是一个很好的。 您对提案的解释是有道理的,尽管我可以看到自己设置了 foo.XY,但没有意识到它会自动更改选择字段。 如果你的提议成功,我仍然会很高兴,即使有一点点保留。

最后,您关于选择嵌入的简单建议似乎是我直觉的建议。 即使我们改变了主意,我们也可以在不破坏现有代码的情况下从简单的提议转变为复杂的提议,但反之则不然。

@ns-cweber

我可以看到自己设置 foo.XY,没有意识到它会自动更改选择字段

这是一个公平的观点,但就此而言,您可以就该语言或任何语言中的很多事情进行讨论。 一般来说,围棋有安全栏杆,但没有安全剪刀。

它通常可以保护您免受许多重大事情的影响,如果您不特意去颠覆它们,但您仍然必须知道自己在做什么。

当您犯这样的错误时,这可能会很烦人,但是,哦,这与“我设置了bar.X = 0但我想设置bar.Y = 0并没有太大不同”,因为假设依赖于您没有意识到foo是一种选择类型。

类似地, i.Foo()p.Foo()v.Foo()看起来都一样,但如果inil接口,则p是一个 nil 指针并且Foo不处理这种情况,前两个可能会恐慌,而如果v使用值方法接收器它不能(至少不是来自调用本身,无论如何) .

至于嵌入,很好的一点是它以后很容易放松,所以我继续编辑了提案。

Sum 类型通常有一个无值字段。 例如,在database/sql包中,我们有:

type NullString struct {
    String string
    Valid  bool // Valid is true if String is not NULL
}

如果我们有和类型/选择/联合,这可能表示为:

type NullString pick {
  Null   struct{}
  String string
}

在这种情况下,和类型比结构具有明显的优势。 我认为这是一个足够常见的用途,值得在任何提案中作为示例包含在内。

Bikeshedding(抱歉),我认为这是值得的语法支持和与 struct 字段嵌入语法的不一致:

type NullString union {
  Null
  String string
}

@尼尔德

首先击中最后一点:作为我发布之前的最后一分钟更改(在任何意义上都不是严格要求的),我补充说,如果有没有字段名称的命名类型(或指向命名类型的指针),pick 将创建一个隐式字段与类型同名。 这可能不是最好的主意,但它似乎可以涵盖“任何这些类型”的常见情况之一,而不会大惊小怪。 鉴于你的最后一个例子可以写成:

type Null = struct{} //though this puts Null in the same scope as NullString
type NullString pick {
  Null
  String string
}

回到你的主要观点,是的,这是一个很好的用途。 实际上,您可以使用它来构建枚举: type Stoplight pick { Stop, Slow, Go struct{} } 。 这很像 const/iota 仿枚举。 它甚至会编译成相同的输出。 在这种情况下的主要好处是代表状态的数字被完全封装,除了列出的三个状态之外,您不能放入任何状态。

不幸的是,在这种情况下,创建和设置Stoplight值的语法有些笨拙:

light := Stoplight{Slow: struct{}{}}
light.Go = struct{}{}

允许{}_struct{}{}简写,正如其他地方所提议的那样,会有所帮助。

许多语言,尤其是函数式语言,通过将标签放在与类型相同的范围内来解决这个问题。 这会产生很多复杂性,并且不允许在同一范围内定义的两个选择共享字段名称。

但是,使用代码生成器很容易解决这个问题,该代码生成器创建一个与选择中每个字段具有相同名称的函数,该函数将字段的类型作为参数。 如果作为特殊情况,如果类型为零大小,则不接受任何参数,则Stoplight示例的输出将如下所示

func Stop() Stoplight {
  return Stoplight{Stop: struct{}{}}
}
func Slow() Stoplight {
  return Stoplight{Slow: struct{}{}}
}
func Go() Stoplight {
  return Stoplight{Go: struct{}{}}
}

对于您的NullString示例,它看起来像这样:

func Null() NullString {
  return NullString{Null: struct{}{}}
}
func String(s string) NullString {
  return NullString{String: s}
}

它不是很漂亮,但它是go generate距离并且很可能很容易内联。

如果它基于类型名称创建隐式字段(除非类型来自其他包)或者它在共享字段名称的同一包中的两个选择上运行,那将不起作用,但这很好。 该提案并不是开箱即用的,但它可以实现很多事情,并让程序员可以灵活地决定什么是最适合给定情况的。

更多语法自行车棚:

type NullString union {
  Null
  Value string
}

var _ = NullString{Null}
var _ = NullString{Value: "some value"}
var _ = NullString{Value} // equivalent to NullString{Value: ""}.

具体而言,具有不包含键的元素列表的文字被解释为命名要设置的字段。

这在语法上与复合文字的其他用途不一致。 另一方面,在 union/pick/sum 类型(至少对我而言)的上下文中,这是一种似乎明智和直观的用法,因为没有键的联合初始值设定项没有合理的解释。

@尼尔德

这在语法上与复合文字的其他用途不一致。

这对我来说似乎是一个巨大的负面影响,尽管它在上下文中确实有意义。

还要注意的是

var ns NullString // == NullString{Null: struct{}{}} == NullString{}
ns.String = "" // == NullString{String: ""}

为了在我使用map[T]struct{}时处理struct{}{} map[T]struct{}我抛出

var set struct{}

某处并使用theMap[k] = set ,类似的将与选择一起使用

进一步的自行车棚:空类型(在 sum 类型的上下文中)通常被命名为“unit”,而不是“null”。

@bcmills排序。

在函数式语言中,当您创建 sum 类型时,它的标签实际上是创建该类型值的函数(尽管编译器知道允许模式匹配的特殊函数称为“类型构造函数”或“tycons”),因此

data Bool = False | True

在同一范围内创建数据类型Bool和两个函数, TrueFalse ,每个函数都带有签名() -> Bool

这里()是你如何编写类型发音单位——只有一个值的类型。 在 Go 中,这种类型可以用多种不同的方式编写,但习惯上写为struct{}

所以构造函数参数的类型将被称为单元。 当用作这样的选项类型时,构造函数名称的约定通常None ,但可以更改以适应域。 例如,如果值来自数据库,则Null将是一个很好的名称。

@bcmills

在我看来,sum-types 的主要优点是它们允许我们区分返回多个值和返回多个值之一——特别是当这些值之一是错误接口的实例时。

从另一个角度来看,我认为这是 Go 中 sum 类型的主要缺点

许多语言当然在返回某个值或错误的情况下使用 sum 类型,这对它们很有效。 如果将 sum 类型添加到 Go 中,那么以相同的方式使用它们将很有诱惑力。

然而,Go 已经有一个庞大的代码生态系统,为此目的使用多个值。 如果新代码使用 sum 类型返回(值,错误)元组,那么该生态系统将变得支离破碎。 一些作者将继续使用多次返回以与他们现有的代码保持一致; 有些作者会使用 sum 类型; 有些人会尝试转换他们现有的 API。 无论出于何种原因,坚持使用旧 Go 版本的作者都将无法使用新的 API。 这将是一团糟,我认为收益将开始不值得付出代价。

如果新代码使用 sum 类型返回(值,错误)元组,那么该生态系统将变得支离破碎。

如果我们在 Go 2 中添加 sum 类型并统一使用它们,那么问题就归结为迁移之一,而不是碎片:需要可以将 Go 1 (value, error) API 转换为 Go 2 (value | error) ) API,反之亦然,但它们在程序的 Go 2 部分中可能是不同的类型。

如果我们在 Go 2 中添加 sum 类型并统一使用

请注意,这是一个与目前看到的提案完全不同的提案:需要对标准库进行大量重构,需要定义 API 样式之间的转换等。走这条路,这将变得非常大和 API 转换的复杂提案,其中包含关于和类型设计的小部分。

目的是让 Go 1 和 Go 2 能够在同一个项目中无缝共存,所以我不认为有人可能“出于某种原因”使用 Go 1 编译器而无法使用去2图书馆。 但是,如果您有依赖关系A依次依赖于B ,并且B更新以在其 API 中使用像pick这样的新功能,那么除非它更新为使用新版本的B否则会破坏依赖关系A BA可以只是供应商B并继续使用旧版本,但如果旧版本没有因安全错误等而维护......或者如果您需要使用新版本B直接,并且由于某种原因,您的项目中不能有两个版本,这可能会产生问题。

归根结底,这里的问题与语言版本无关,而与更改现有导出函数的签名有关。 事实上,这将是一个提供动力的新功能,这有点让人分心。 如果目的允许将现有 API 更改为使用pick而不破坏向后兼容性,那么可能需要某种桥接语法。 例如(完全作为一个稻草人):

type ReadResult pick(N int, Err error) {
    N
    PartialResult struct { N; Err }
    Err
}

当遗留代码访问ReadResult时,编译器可以只使用template.Must这样的 API 可能只需要继续接受多个值而不是pick并依靠 splatting 来弥补差异。 或者可以使用这样的东西:

type ReadResult pick(N int, Err error) {
case Err == nil:
    N
default:
    PartialResult struct { N; Err }
case N == 0:
    Err
}

这并不复杂的事情,但我可以看到如何引入一个功能,它改变的API应该怎么写需要如何转变的故事没有打破的世界。 也许有一种不需要桥接语法的方法。

从总和类型到乘积类型(结构体、多个返回值)很简单——只需将不是值的所有内容都设置为零。 从乘积类型到总和类型的定义一般不明确。

如果 API 希望从基于产品类型的实现无缝逐渐过渡到基于和类型的实现,最简单的方法是拥有所有必需的两个版本,其中和类型版本具有实际实现,产品类型版本调用sum 类型版本,执行任何运行时检查需要和任何投影到产品空间。

这真的很抽象,所以这里有一个例子

没有总和的版本 1

func Take(i interface{}) error {
  switch i.(type) {
  case int: //do something
  case string:
  default: return fmt.Errorf("invalid %T", i)
  }
}
func Give() (interface{}, error) {
   i := f() //something
   if i == nil {
     return nil, errors.New("whoops v:)v")
  }
  return i
}

带总和的版本 2

type Value pick {
  I int
  S string
}
func TakeSum(v Value) {
  // do something
}
// Deprecated: use TakeSum
func Take(i interface{}) error {
  switch x := i.(type) {
  case int: TakeSum(Value{I: x})
  case string: TakeSum(Value{S: x})
  default: return fmt.Errorf("invalid %T", i)
  }
}
type ErrValue pick {
  Value
  Err error
}
func GiveSum() ErrValue { //though honestly (Value, error) is fine
  return f()
}
// Deprecated: use GiveSum
func Give() (interface{}, error) {
  switch v := GiveSum().(var) {
  case Value:
    switch v := v.(var) {
    case I: return v, nil
    case S: return v, nil
    }
  case Err:
    return nil, v
  }
}

版本 3 将删除给予/接受

版本 4 将 GiveSum/TakeSum 的实现移至 Give/Take,使 GiveSum/TakeSum 只调用 Give/Take 并弃用 GiveSum/TakeSum。

版本 5 将删除 GiveSum/TakeSum

它并不漂亮或快速,但它与任何其他类似性质的大规模中断相同,并且不需要任何额外的语言

我认为(大部分) sum 类型的效用可以通过一种机制来实现,这种机制在编译时限制对类型 interface{} 的赋值。

在我的梦中它看起来像:

type T1 switch {T2,T3} // only nil, T2 and T3 may be assigned to T1
type T2 struct{}
type U switch {} // only nil may be assigned to U
type V switch{interface{} /* ,... */} // V equivalent to interface{}
type Invalid switch {T2,T2} // only uniquely named types
type T3 switch {int,uint} // switches can contain switches but... 

...断言开关类型是未显式定义的类型也将是编译时错误:

var t1 T1
i,ok := t1.(int) // T1 can't be int, only T2 or T3 (but T3 could be int)
switch t := t1.(type) {
    case int: // compile error, T1 is just nil, T2 or T3
}

和 go vet 会讨论像 T3 这样的类型的模棱两可的常量分配,但出于所有意图和目的(在运行时) var x T3 = 32将是var x interface{} = 32 。 也许一些名为 switch 或 ponies 之类的包中的内置函数的预定义开关类型也很时髦。

@ J7B,@ianlancetaylor在提供了类似的想法https://github.com/golang/go/issues/19412#issuecomment -323256891

我在https://github.com/golang/go/issues/19412#issuecomment -325048452 上发布了我认为的逻辑后果

考虑到相似性,它们中的许多看起来都同样适用。

如果这样的事情能奏效,那就太好了。 从接口转换到接口+限制会很容易(尤其是使用 Ian 的语法:只需在使用接口构建的现有伪和的末尾添加restrict )。 这很容易实现,因为在运行时它们基本上与接口相同,并且大部分工作只是让编译器在它们的不变量被破坏时发出额外的错误。

但我认为不可能让它发挥作用。

一切都排得很近,看起来很合身,但是放大后发现不太对劲,所以你稍微推动一下,然后其他东西就会不对齐。 你可以尝试修复它,但是你会得到一些看起来很像接口但在奇怪的情况下表现不同的东西。

也许我错过了一些东西。

受限接口提案没有任何问题,只要您对案例不一定不相交感到满意。 我认为两种接口类型(如io.Reader / io.Writer )之间的联合并没有脱节,这并不像您所做的那样令人惊讶。 这与您无法确定分配给interface{}的值是否存储为io.Readerio.Writer如果它同时实现)的事实完全一致。 只要每种情况都是具体类型,您就可以构造不相交的联合这一事实似乎完全足够。

权衡是,如果联合是受限制的接口,那么您不能直接在它们上定义方法。 如果它们是受限制的接口类型,您将无法获得pick类型提供的有保证的直接存储。 我不确定是否值得在语言中添加一种独特的东西来获得这些额外的好处。

@jimmyfrasche对于type T switch {io.Reader,io.Writer}可以将 ReadWriter 分配给 T,但您只能断言 T 是 io.Reader 或 Io.Writer,您需要另一个断言来断言 io.Reader 或 io.Writer 是一个 ReadWriter,如果它是一个有用的断言,它应该鼓励将它添加到 switchtype。

@stevenblenkinsop您可以没有方法的情况下定义

而且,另一方面, @ianlancetaylor的语法将允许

type IR interface {
  Foo()
  Bar()
} restrict { A, B, C }

只要ABC都可以编译,每个都有FooBar方法(尽管你不得不担心大约nil值)。

编辑:斜体说明

我认为某种形式的 _restricted interface_ 会很有用,但我不同意语法。 这是我的建议。 它的作用类似于代数数据类型,它将不一定具有共同行为的域相关对象分组。

//MyGroup can be any of these. It can contain other groups, interfaces, structs, or primitive types
type MyGroup group {
   MyOtherGroup
   MyInterface
   MyStruct
   int
   string
   //..possibly some other types as well
}

//type definitions..
type MyInterface interface{}
type MyStruct struct{}
//etc..

func DoWork(item MyGroup) {
   switch t:=item.(type) {
      //do work here..
   }
}

与传统的空接口interface{}方法相比,这种方法有几个好处:

  • 使用函数时的静态类型检查
  • 用户可以仅从函数签名中推断出需要什么类型的参数,而无需查看函数实现

当涉及的类型数量未知时,空接口interface{}很有用。 你真的别无选择,只能依靠运行时验证。 另一方面,当编译时类型数量有限且已知时,为什么不让编译器来帮助我们?

@henryas我认为更有用的比较是目前推荐的(开放)和类型的方法:非空接口(如果没有清晰的接口可以提取,使用未导出的标记函数)。
我不认为你的论点在很大程度上适用于此。

下面是一份关于 Go protobufs 的体验报告:

  • proto2 语法允许“可选”字段,这些字段是零值和未设置值之间存在区别的类型。 当前的解决方案是使用指针(例如, *int ),其中 nil 指针表示未设置,而 set 指针指向实际值。 希望是一种允许区分零和未设置的方法,而不会使仅需要访问值的常见情况复杂化(如果未设置,零值就可以了)。

    • 由于额外的分配,这是无效的(尽管联合可能会遭受相同的命运,具体取决于实施)。
    • 这对用户来说很痛苦,因为需要不断检查指针会损害可读性(尽管 protos 中的非零默认值可能意味着需要检查是一件好事......)。
  • proto 语言允许“one ofs”,这是 sum 类型的 proto 版本。 目前采取的方法如下(粗略的例子):

    • 使用隐藏方法定义接口类型(例如, type Communique_Union interface { isCommunique_Union() }
    • 对于联合中允许的每个可能的 Go 类型,定义一个包装结构,其唯一目的是包装每个允许的类型(例如type Communique_Number struct { Number int32 } ),其中每个类型都有isCommunique_Union方法。
    • 这也是无效的,因为包装器会导致分配。 sum 类型会有所帮助,因为我们知道最大值(切片)不会超过 24B。

@henryas我认为更有用的比较是目前推荐的(开放)和类型的方法:非空接口(如果没有清晰的接口可以提取,使用未导出的标记函数)。
我不认为你的论点在很大程度上适用于此。

您的意思是向对象添加一个虚拟的未导出方法,以便该对象可以作为接口传递,如下所示?

type MyInterface interface {
   belongToMyInterface() //dummy method definition
}

type MyObject struct{}
func (MyObject) belongToMyInterface(){} //dummy method

我认为根本不应该推荐。 它更像是一种解决方法而不是解决方案。 我个人宁愿放弃静态类型验证,也不愿拥有空方法和不必要的方法定义。

这些是_dummy method_方法的问题:

  • 不必要的方法和方法定义使对象和接口混乱。
  • 每次添加新的 _group_ 时,您都需要修改对象的实现(例如,添加虚拟方法)。 这是错误的(见下一点)。
  • 代数数据类型(或基于 _domain_ 而不是行为的分组)是特定interface{}方法更糟糕。 有更好的方法可用。

@亨利亚斯

我不认为你的第三点是强有力的论据。 如果会计师想要以不同的方式查看对象关系,那么会计师可以创建符合其规范的自己的界面。 向接口添加私有方法并不意味着满足它的具体类型与别处定义的接口子集不兼容。

Go 解析器大量使用了这种技术,老实说,我无法想象选择使该包变得如此之好,以至于它保证在语言中实现选择。

@as我的观点是,每次创建新的 _relationship view_ 时,都必须更新相关的具体对象,以便为该视图做出一定的调整。 这似乎是错误的,因为为了做到这一点,对象必须经常对消费者的领域做出一定的假设。 如果对象和消费者密切相关或生活在同一个域中,例如在 Go 解析器的情况下,则可能无关紧要。 但是,如果这些对象提供要由其他几个域使用的基本功能,就会成为一个问题。 对象现在需要对_dummy method_ 方法工作的所有其他域有所了解。

您最终将许多空方法附加到对象上,并且读者不清楚为什么需要这些方法,因为需要它们的接口位于单独的域/包/层中。

open-sums-via-interfaces 方法不能让您轻松¹ 使用总和这一点是足够公平的。 显式和类型显然会使求和变得更容易。 不过,这是一个与“总和类型为您提供类型安全性”非常不同的论点 - 如果需要,您今天仍然可以获得类型安全性。

不过,我仍然看到在其他语言中实现的封闭总和的两个缺点:一,在大规模分布式开发过程中难以发展它们。 第二,我认为它们为类型系统增加了力量,我喜欢Go 没有非常强大的类型系统,因为它不鼓励编码类型,而是编码程序 - 当我觉得问题可以从更强大的类型系统,我转向更强大的语言(如 Haskell 或 Rust)。

话虽如此,至少第二个绝对是首选之一,即使您同意,是否认为弊大于利也取决于个人喜好。 只是想指出,如果没有封闭的总和类型,您就无法获得类型安全的总和并不是真的:)

[1] 值得注意的是,这并不容易,但仍然有可能,例如你可以做到

type Node interface {
    node()
}

type Foo struct {
    bar.Baz
}

func (foo) node() {}

@Merovius
我不同意你的第二个缺点。 事实上,标准库中有很多地方可以从 sum 类型中受益匪浅,但现在使用空接口和恐慌来实现,这表明这种缺乏正在损害编码。 当然,人们可能会说,既然这样的代码已经写好了,那就没有问题了,我们不需要 sum 类型,但这种逻辑的愚蠢之处在于,我们不需要任何其他类型的函数签名,我们应该只使用空接口。

至于现在使用带有某种方法的接口来表示和类型,有一个很大的缺点。 您不知道该接口可以使用哪些类型,因为它们是隐式实现的。 使用正确的 sum 类型,类型本身准确地描述了实际可以使用的类型。

我不同意你的第二个缺点。

您是否不同意“和类型鼓励使用类型编程”的说法,或者您不同意这是一个缺点? 因为您似乎并不反对第一个(您的评论基本上只是重申了这一点),关于第二个,我承认这取决于上面的偏好。

事实上,标准库中有很多地方可以从 sum 类型中受益匪浅,但现在使用空接口和恐慌来实现,这表明这种缺乏正在损害编码。 当然,人们可能会说,既然这样的代码已经写好了,那就没有问题了,我们不需要 sum 类型,但这种逻辑的愚蠢之处在于,我们不需要任何其他类型的函数签名,我们应该只使用空接口。

这种黑白分明的论点并没有真正的帮助。 我同意,在某些情况下,总和类型会减轻痛苦。 使类型系统更强大的每次更改都会在某些情况下减轻痛苦 - 但在某些情况下也会引起痛苦。 所以问题是,哪个比另一个更重要(也就是说,在很大程度上,一个偏好问题)。

讨论不应该是关于我们是否想要一个 python 式的类型系统(无类型)或一个 coq 式的类型系统(所有内容的正确性证明)。 讨论应该是“求和类型的好处是否超过它们的缺点”,承认两者是有帮助的。


FTR,我想再次强调,就我个人而言,我不会那么反对开放和类型(即每个和类型都有一个隐式或显式的“SomethingElse”-case),因为它会减轻大多数技术缺点它们(主要是它们很难发展)同时还提供了它们的大部分技术优势(静态类型检查,您提到的文档,您可以从其他包中枚举类型......)。

不过,我也假设,对于那些通常推动和类型的人来说,开放和 a) 不会是令人满意的妥协,并且 b) 可能不会被认为是一个足够大的好处,以保证 Go 团队将其包含在内。 但我已经准备好在这些假设中的一个或两个上被证明是错误的:)

还有一个问题:

事实上,标准库中有很多地方可以从 sum 类型中受益匪浅

我只能想到标准库中的两个地方,我认为它们有任何显着的好处:reflect 和 go/ast。 即使在那里,如果没有它们,这些软件包似乎也能正常工作。 从这个参考点来看,“充足”和“非常”这两个词似乎有些夸大其词——当然,我可能看不到一堆合法的地方。

database/sql/driver.Value可能会从 sum 类型中受益(如 #23077 中所述)。
https://godoc.corp.google.com/pkg/database/sql/driver#Value

然而, database/sql.Rows.Scan更公共的接口不会在功能上有所损失。 Scan 可以读入基础类型为例如int ; 将其目标参数更改为 sum 类型需要将其输入限制为一组有限的类型。
https://godoc.corp.google.com/pkg/database/sql#Rows.Scan

@Merovius

我不会那么反对开放和类型(即每个和类型都有一个隐式或显式的“SomethingElse”-case),因为它会减轻它们的大部分技术缺点(主要是它们很难发展)

至少还有两个其他选择可以缓解封闭和的“难以进化”问题。

一种是允许匹配实际上不属于总和的类型。 然后,要将成员添加到总和中,您首先更新其消费者以与新成员匹配,并且只有在消费者更新后才实际添加该成员。

另一个是允许“不可能”的成员:即在匹配中明确允许但在实际值中明确不允许的成员。 要将成员添加到总和中,首先将其添加为不可能成员,然后更新消费者,最后将新成员更改为可能成员。

database/sql/driver.Value可能会从总和类型中受益

同意,不知道那个。 谢谢 :)

一种是允许匹配实际上不属于总和的类型。 然后,要将成员添加到总和中,您首先更新其消费者以与新成员匹配,并且只有在消费者更新后才实际添加该成员。

有趣的解决方案。

@Merovius接口本质上是一系列无限和类型。 所有和类型,无限或其他,都有一个default:大小写。 但是,如果没有有限和类型, default:意味着要么是您不知道的有效案例,要么是作为程序中某个错误的无效案例——对于有限和,它只是前者,而绝不是后者。

json.Token 和 sql.Null* 类型是其他规范示例。 go/types 会像 go/ast 一样受益。 我猜有很多示例不在导出的 API 中,通过限制内部状态的域,可以更轻松地调试和测试一些复杂的管道。 我发现它们对于在通用库的公共 API 中不经常出现的内部状态和应用程序约束最有用,尽管它们偶尔也会在那里使用。

我个人认为 sum 类型给 Go 提供了足够的额外功能,但不会太多。 Go 类型系统已经非常好和灵活,尽管它也有它的缺点。 类型系统中添加的 Go2 不会像现有的那样提供那么多的功能——80-90% 的需求已经到位。 我的意思是,即使是泛型也不会从根本上让你做一些新的事情:它会让你做你已经做的事情,更安全、更容易、更高效,并且可以实现更好的工具。 Sum 类型是相似的,imo(尽管很明显,如果它是一个或其他泛型将优先(并且它们配对得很好))。

如果您在 sum 类型开关上允许一个无关的默认值(所有 case + default 被允许),并且没有让编译器强制执行穷举(尽管 linter 可以),那么将 case 添加到 sum 也同样容易(也同样困难) ) 作为更改任何其他公共 API。

json.Token 和 sql.Null* 类型是其他规范示例。

令牌 - 当然。 AST 问题的另一个实例(基本上任何解析器都可以从 sum 类型中受益)。

不过,我看不到 sql.Null* 的好处。 如果没有泛型(或添加一些“神奇的”泛型可选内置),您仍然必须拥有类型,并且type NullBool enum { Invalid struct{}; Value Int }type NullBool struct { Valid bool; Value Int }之间似乎没有显着差异。 是的,我知道有区别,但它微乎其微。

如果您在 sum 类型开关上允许一个无关的默认值(所有 case + default 被允许),并且没有让编译器强制执行穷举(尽管 linter 可以),那么将 case 添加到 sum 也同样容易(也同样困难) ) 作为更改任何其他公共 API。

看上面。 这些就是我所说的公开金额,我不太反对它们。

这些就是我所说的公开金额,我不太反对它们。

我的具体建议是https://github.com/golang/go/issues/19412#issuecomment -323208336,我相信它可能满足你对开放的定义,虽然它仍然有点粗糙,我相信还有更多去除和抛光。 特别是我注意到,即使列出了所有案例,也不清楚是否可以接受默认案例,所以我只是更新了它。

同意可选类型不是和类型的杀手级应用。 不过它们非常好,正如您指出的泛型定义了一个

type Nullable(T) pick { // or whatever syntax (on all counts)
  Null struct{}
  Value T
}

一次并涵盖所有情况会很棒。 但是,正如您所指出的,我们可以对通用产品 (struct) 做同样的事情。 存在 Valid = false, Value != 0 的无效状态。在这种情况下,如果这会导致问题,很容易根除,因为 2 ⨯ T 很小,即使它没有 1 + T 小。

当然,如果它是一个更复杂的总和,有很多情况和许多重叠的不变量,那么即使使用防御性编程也更容易犯错误,也更难发现错误,所以让不可能的事情根本不编译可以节省很多头发拉。

令牌 - 当然。 AST 问题的另一个实例(基本上任何解析器都可以从 sum 类型中受益)。

我写了很多程序,这些程序接受一些输入,做一些处理,并产生一些输出,我通常将它递归地分成很多遍,将输入划分为案例,并根据这些案例将其转换为越来越接近所需的输出。 我可能不是真的在写解析器(诚然有时我是因为那很有趣!)但我发现 AST 问题,正如你所说的,适用于很多代码——尤其是在处理具有太多奇怪的深奥业务逻辑时要求和边缘情况适合我的小脑袋。

当我编写一个通用库时,它不会像执行某些 ETL 或制作一些奇特的报告或确保处于状态 X 的用户在未标记为 Z 的情况下执行 Y 操作那样经常出现在 API 中。一个通用库虽然我发现能够限制内部状态的地方会有所帮助,即使它只是将 10 分钟的调试减少到 1 秒“哦,编译器说我错了”。

特别是在 Go 中,我使用 sum 类型的一个地方是一个 goroutine 在一堆通道中选择,我需要为一个 goroutine 提供 3 个通道,为另一个 goroutine 提供 2 个通道。 这将帮助我跟踪发生了什么事情才能够使用chan pick { a A; b B; c C }超过chan Achan Bchan C虽然chan stuct { kind MsgKind; a A; b B; c C }能以额外的空间和更少的验证为代价,在紧要关头完成这项工作。

作为对现有接口类型切换功能的补充,而不是新类型如何编译时类型列表检查?

func main() {
    if FlipCoin() == false {
        printCertainTypes(FlipCoin(), int(5))
    } else {
        printCertainTypes(FlipCoin(), string("5"))
    }
}
// this function compiles with main
func printCertainTypes(flip bool, in interface{}) {
    if flip == false {
        switch v := in.(type) {
        case int:
            fmt.Printf(“integer %v\n”, v)
        default:
            fmt.Println(v)
        }
    } else {
        switch v := in.(type) {
        case int:
            fmt.Printf(“integer %v\n”, v)
        case string:
            fmt.Printf(“string %v\n”, v)
        }
    }
}
// this function compiles with main
func printCertainTypes(flip bool, in interface{}) {
    switch v := in.(type) {
    case int:
        fmt.Printf(“integer %v\n”, v)   
    case bool:
        fmt.Printf(“bool %v\n”, v)
    }
    fmt.Println(flip)
    switch v := in.(type) {
    case string:
        fmt.Printf(“string %v\n”, v)
    case bool:
        fmt.Printf(“bool 2 %v\n”, v)
    }
}
// this function emits a type switch not complete error when compiled with main
func printCertainTypes(flip bool, in interface{}) {
    if flip == false {
        switch v := in.(type) {
        case int:
            fmt.Printf(“integer %v\n”, v)
        case bool:
            fmt.Printf(“bool %v\n”, v)
        }
    } else {
        switch v := in.(type) {
        case string:
            fmt.Printf(“string %v\n”, v)
        case bool:
            fmt.Printf(“bool %v\n”, v)
        }
    }
}
// this function emits a type switch not complete error when compiled with main
func printCertainTypes(flip bool, in interface{}) {
    fmt.Println(flip)
    switch v := in.(type) {
    case int:
        fmt.Printf(“integer %v\n”, v)
    case bool:
        fmt.Printf(“bool %v\n”, v)
    }
}

公平地说,我们应该探索在当前类型系统中近似和类型的方法并权衡它们的利弊。 如果不出意外,它提供了比较的基线。

标准手段是一个接口,带有一个未导出的、什么都不做的方法作为标签。

反对这一点的一个论点是总和中的每种类型都需要在其上定义此标记。 这不是严格正确的,至少对于结构体成员,我们可以这样做

type Sum interface { sum() }
type sum struct{}
func (sum) sum() {}

并将该 0-width 标签嵌入到我们的结构中。

我们可以通过引入包装器将外部类型添加到我们的总和中

type External struct {
  sum
  *pkg.SomeType
}

虽然这有点难看。

如果 sum 中的所有成员都有共同的行为,我们可以在接口定义中包含这些方法。

像这样的构造让我们说一个类型在一个总和中,但它并没有让我们说那个总和中没有什么。 除了强制性的nil情况外,外部包还可以使用相同的嵌入技巧,例如

import "p"
var member struct {
  p.Sum
}

在包中,我们必须注意验证编译但非法的值。

有多种方法可以在运行时恢复某些类型安全。 我发现在 sum 接口的定义中包含一个valid() error方法和一个类似的 func

func valid(s Sum) error {
  switch s.(type) {
  case nil:
    return errors.New("pkg: Sum must be non-nil")
  case A, B, C, ...: // listing each valid member
    return s.valid()
  }
  return fmt.Errorf("pkg: %T is not a valid member of Sum")
}

很有用,因为它允许同时处理两种验证。 对于碰巧总是有效的成员,我们可以避免一些样板文件

type alwaysValid struct{}
func (alwaysValid) valid() error { return nil }

关于这种模式的一个更常见的抱怨是它没有在 godoc 中清楚地说明总和中的成员资格。 由于它也不允许我们排除成员并要求我们进行验证,因此有一个简单的方法来解决这个问题:导出虚拟方法。
代替,

//A Node is one of (list of types).
type Node interface { node() }

//A Node is only valid if it is defined in this package.
type Node interface { 
  //Node is a dummy method that signifies that a type is a Node.
  Node()
}

我们不能阻止任何人满足Node所以我们不妨让他们知道是什么。 虽然这并不能一目了然地说明哪些类型满足Node (没有中央列表),但它确实清楚地表明您现在正在查看的特定类型是否满足Node

当 sum 中的大多数类型都定义在同一个包中时,此模式很有用。 如果没有,通常的方法是退回到interface{} ,例如json.Tokendriver.Value 。 我们可以将前面的模式与每个包装类型一起使用,但最后它说interface{}这么多,所以没有什么意义。 如果我们期望这些值来自包之外,我们可以礼貌地定义一个工厂:

//Sum is one of int64, float64, or bool.
type Sum interface{}
func New(v interface{}) (Sum, error) {
  switch v.(type) {
  case nil:
    return errors.New("pkg: Sum must be non-nil")
  case int64, float64, bool:
     return v
  }
  return fmt.Printf("pkg: %T is not a valid member of Sum")
}

sums 的常见用途是用于可选类型,您需要区分“无值”和“可能为零的值”。 有两种方法可以做到这一点。

*T让你表示没有值作为nil指针和(可能)零值作为取消引用非零指针的结果。

与之前的基于接口的近似以及将和类型实现为有限制的接口的各种建议一样,这需要额外的指针取消引用和可能的堆分配。

对于可选项,可以使用 sql 包中的技术避免这种情况

type OptionalT struct {
  Valid bool
  Value T
}

这样做的主要缺点是它允许对无效状态进行编码:Valid 可以为 false,Value 可以为非零。 当 Valid 为 false 时,也可以获取 Value (尽管如果您想要零 T 如果未指定,这可能很有用)。 随意将 Valid 设置为 false 而不将 Value 归零,然后将 Valid 设置为 true(或忽略它)而不分配 Value 会导致先前丢弃的值意外重新出现。 这可以通过提供 setter 和 getter 来保护类型的不变量来解决。

和类型的最简单形式是当您关心身份而不是值时:枚举。

在 Go 中处理这个问题的传统方法是 const/iota:

type Enum int
const (
  A Enum = iota
  B
  C
)

OptionalT类型一样,它没有任何不必要的间接寻址。 就像接口 sums 一样,它不限制域:只有三个有效值和许多无效值,因此我们需要在运行时进行验证。 如果正好有两个值,我们可以使用 bool。

还有这种类型的基本数量问题。 A+B == C 。 我们可以很容易地将无类型的整数常量转换为这种类型。 有很多地方是可取的,但无论如何我们都会得到这个。 通过一些额外的工作,我们可以将其限制为身份:

type Enum struct { v int }
var (
  A = Enum{0}
  B = Enum{1}
  C = Enum{2}
)

现在这些只是不透明的标签。 它们可以进行比较,但仅此而已。 不幸的是,现在我们失去了常量性,但我们可以通过更多的工作来恢复它:

func A() Enum { return Enum{0} }
func B() Enum { return Enum{1} }
func C() Enum { return Enum{2} }

我们已经恢复了外部用户无法以一些样板和一些高度内联的函数调用为代价来更改名称的情况。

然而,这在某些方面比接口总和更好,因为我们几乎完全关闭了类型。 外部代码只能使用A()B()C() 。 他们不能像 var 示例中那样交换标签,也不能执行A() + B()并且我们可以在Enum上自由定义我们想要的任何方法。 同一个包中的代码仍然有可能错误地创建或修改一个值,但如果我们注意确保不会发生这种情况,这是第一个不需要验证代码的和类型:如果存在,则它是有效的.

有时你有很多标签,其中一些有额外的日期,而那些有相同类型的数据。 假设您有一个值具有三个无值状态(A、B、C),两个具有字符串值(D、E),一个具有字符串值和一个 int 值(F)。 我们可以使用上述策略的多种组合,但最简单的方法是

type Value struct {
  Which int // could have consts for A, B, C, D, E, F
  String string
  Int int
}

这很像上面的OptionalT类型,但它有一个枚举而不是 bool 并且有多个字段可以设置(或不设置)取决于Which 。 验证必须注意这些设置(或不)适当。

在 Go 中有很多方法可以表达“以下之一”。 有些人比其他人需要更多的照顾。 它们通常需要在运行时验证“其中之一”不变式或无关的取消引用。 他们共有的一个主要缺点是,由于它们是在语言中模拟而不是语言的一部分,因此“一个”不变量不会出现在反射或 go/types 中,这使得元编程变得困难他们。 要在元编程中使用它们,你们都需要能够识别和验证 sum 的正确风格,并被告知这就是您正在寻找的东西,因为它们看起来很像没有“之一”不变式的有效代码。

如果 sum 类型是语言的一部分,它们可以被反映并轻松地从源代码中提取出来,从而产生更好的库和工具。 如果编译器知道“其中之一”不变量,它可以进行许多优化。 程序员可以专注于重要的验证代码,而不是检查一个值确实在正确的域中的琐碎维护。

像这样的构造让我们说一个类型在一个总和中,但它并没有让我们说那个总和中没有什么。 除了强制 nil 情况外,外部包也可以使用相同的嵌入技巧,例如
[…]
在包中,我们必须注意验证编译但非法的值。

为什么? 作为一个包作者,这对我来说似乎是“你的问题”的范畴。 如果你给我一个io.Reader ,它的Read方法恐慌,我不会从中恢复,只是让它恐慌。 同样,如果您不遗余力地创建我声明的类型的无效值 - 我该和谁争论? 即我认为“我嵌入了一个模拟封闭和”是一个很少(如果有的话)偶然出现的问题。

话虽如此,您可以通过将接口更改为type Sum interface { sum() Sum }并让每个值返回自身来防止该问题。 这样,您可以只使用sum()的返回值,即使在嵌入的情况下它也会表现良好。

关于这种模式的一个更常见的抱怨是它没有在 godoc 中清楚地说明总和中的成员资格。

这可能对您有所帮助

这样做的主要缺点是它允许对无效状态进行编码:Valid 可以为 false,Value 可以为非零。

这对我来说不是无效状态。 零值并不神奇。 IMO, sql.NullInt64{false,0}NullInt64{false,42}之间没有区别。 两者都是 SQL NULL 的有效和等效表示。 如果所有代码在使用 Value 之前都检查 Valid,则程序无法观察到差异。

编译器没有强制执行此检查(对于“真正的”选项/和类型,它可能会这样做),这使得这样做更容易,这是一个公平而正确的批评。 但是,如果您确实忘记了它,我认为意外使用零值比意外使用非零值更好(指针形状的类型可能除外,因为它们在使用时会出现恐慌,因此大声失败 - 但对于那些,你应该只使用裸指针形状的类型,并使用nil作为“未设置”)。

还有这种类型的基本数量问题。 A+B == C。我们可以很容易地将无类型整数常量转换为这种类型。

这是一个理论上的问题还是在实践中出现了?

程序员可以专注于重要的验证代码,而不是检查一个值确实在正确的域中的琐碎维护。

只是 FTR,在我确实使用 sum-types-as-sum-types 的情况下(即问题不能通过黄金品种接口更优雅地建模),我从不编写任何验证代码。 就像我不检查作为接收器或参数传递的指针是否为零一样(除非它被记录为有效的变体)。 在编译器强迫我处理的地方(即“函数结束时不返回”样式问题),我在默认情况下感到恐慌。

就我个人而言,我认为 Go 是一种实用语言,它不仅仅是为了自己的利益或因为“每个人都知道他们更好”而添加安全功能,而是基于已证明的需求。 我认为以务实的方式使用它是很好的。

标准手段是一个接口,带有一个未导出的、什么都不做的方法作为标签。

接口和 sum 类型之间存在根本区别(我在您的帖子中没有看到它)。 当您通过接口近似求和类型时,实际上无法处理该值。 作为消费者,您不知道它实际持有什么,只能猜测。 这并不比仅使用空接口好。 唯一有用的是,如果任何实现只能来自定义接口的同一个包,因为只有这样你才能控制你能得到什么。

另一方面,有类似的东西:

func foo(val string|int|error) {
    switch v:= val.(type) {
    case string:
        ...
    }
}

赋予消费者使用 sum 类型值的全部权力。 它的价值是具体的,不可解释。

@Merovius
您提到的这些“未结金额”具有某些人可能归类为重大缺点的内容,因为它们会允许滥用它们进行“功能蠕变”。 这正是为什么可选函数参数被拒绝作为功能的原因。

您提到的这些“未结金额”具有某些人可能归类为重大缺点的内容,因为它们会允许滥用它们进行“功能蠕变”。 这正是为什么可选函数参数被拒绝作为功能的原因。

这对我来说似乎是一个非常弱的论点 - 如果没有别的,那么因为它们存在,所以你已经允许它们启用任何东西。 事实上,我们已经有了可选参数,用于所有意图和目的(不是我喜欢这种模式,但它显然已经在语言中成为可能)。

接口和 sum 类型之间存在根本区别(我在您的帖子中没有看到它)。 当您通过接口近似求和类型时,实际上无法处理该值。 作为消费者,您不知道它实际持有什么,只能猜测。

我已经尝试第二次解析它,但仍然不能。 为什么你不能使用它们? 它们可以是常规的导出类型。 是的,它们必须是在您的包中创建的类型(显然),但除此之外,与实际的封闭总和相比,您如何使用它们似乎没有任何限制。

我已经尝试第二次解析它,但仍然不能。 为什么你不能使用它们? 它们可以是常规的导出类型。 是的,它们必须是在您的包中创建的类型(显然),但除此之外,与实际的封闭总和相比,您如何使用它们似乎没有任何限制。

当dummy方法被导出并且任何第三方都可以实现“sum类型”时会发生什么? 或者一个非常现实的场景,一个团队成员不熟悉接口的各种消费者,决定在同一个包中添加另一个实现,并且该实现的实例最终通过各种代码方式传递给这些消费者? 冒着重复我明显的“无法解析”声明的风险:“作为消费者,您不知道 [总和值] 实际持有什么,只能猜测。”。 你知道,因为它是一个接口,它不会告诉你是谁在实现它。

@Merovius

只是 FTR,在我确实使用 sum-types-as-sum-types 的情况下(即问题不能通过黄金品种接口更优雅地建模),我从不编写任何验证代码。 就像我不检查作为接收器或参数传递的指针是否为零一样(除非它被记录为有效的变体)。 在编译器强迫我处理的地方(即“函数结束时不返回”样式问题),我在默认情况下感到恐慌。

我不认为这是永远永远不会发生的事情。

如果有人传递错误的输入会立即爆炸,我不会理会验证代码。

但是如果有人传递错误的输入最终可能会导致恐慌但它不会出现一段时间,那么我编写验证代码以便尽快标记错误输入并且没有人必须弄清楚错误是引入的 150调用堆栈中的帧(特别是因为他们可能必须在调用堆栈中再上升 150 帧才能找出引入错误值的位置)。

现在花半分钟可能节省半小时的调试时间是务实的。 尤其是对我来说,因为我总是犯愚蠢的错误,而且我越早接受教育,我就能越早犯下一个愚蠢的错误。

如果我有一个接受阅读器并立即开始使用它的 func,我不会检查 nil,但是如果 func 是一个结构的工厂,在调用某个方法之前不会调用阅读器,我会检查它是否为 nil 和 panic 或者返回一个类似“reader must not be nil”这样的错误,以便错误的原因尽可能接近错误的来源。

godoc分析

我知道,但我觉得它没有用。 在我点击 ^C 之前,它在我的工作区运行了 40 分钟,每次安装或修改包时都需要刷新。 不过有#20131(从这个线程分叉出来!)。

话虽如此,您可以通过将接口更改为type Sum interface { sum() Sum }并让每个值返回自身来防止该问题。 这样,您可以只使用sum()的返回值,即使在嵌入的情况下也会表现良好。

我还没有发现那有用。 它没有提供比显式验证更多的好处,它提供的验证更少。

[您可以添加 const/iota 枚举成员的事实] 是理论上的问题还是已在实践中出现?

那个特别的是理论性的:我试图列出我能想到的所有优点和缺点,理论和实践。 不过,我更重要的一点是,有很多方法可以尝试表达语言中的“一个”不变量,这些方法确实得到了相当普遍的使用,但没有一种方法像语言中的一种类型那样简单。

[您可以将无类型积分分配给 const/iota 枚举的事实]是理论上的问题还是在实践中出现过?

在实践中已经出现了这一点。 很快就找出出了什么问题,但如果编译器说“那里,那条线——那是那条错了”,它会花费更少的时间。 有关于处理这种特殊情况的其他方法的讨论,但我不知道它们会有什么普遍用途。

这对我来说不是无效状态。 零值并不神奇。 IMO, sql.NullInt64{false,0}NullInt64{false,42}之间没有区别。 两者都是 SQL NULL 的有效和等效表示。 如果所有代码在使用 Value 之前都检查 Valid,则程序无法观察到差异。

编译器没有强制执行此检查(对于“真正的”选项/和类型,它可能会这样做),这使得不这样做更容易,这是一个公平而正确的批评。 但是,如果您确实忘记了它,我认为意外使用零值比意外使用非零值更好(指针形状的类型可能除外,因为它们在使用时会出现恐慌,因此大声失败 - 但对于那些,你应该只使用裸指针形状的类型,并使用 nil 作为“未设置”)。

“如果所有代码在使用 Value 之前都检查 Valid”是错误所在以及编译器可以强制执行的操作。 我遇到过这样的错误(尽管该模式的版本更大,其中有多个值字段和两个以上的鉴别器状态)。 我相信/希望我在开发和测试过程中发现了所有这些,并且没有一个逃到野外,但是如果编译器能够在我犯那个错误时告诉我并且我可以确定其中一个的唯一方法,那就太好了忽略了编译器中是否存在错误,就像我尝试将字符串分配给 int 类型的变量时它会告诉我的方式一样。

而且,当然,我更喜欢*T用于可选类型,尽管在执行时空和代码可读性方面确实具有与之相关的非零成本。

(对于该特定示例,通过选择建议获取实际值或正确零值的代码将是v, _ := nullable.[Value] ,它简洁且安全。)

那不是我想要的。 选择类型应该是值类型,
就像在 Rust 中一样。 如果需要,它们的第一个字应该是指向 GC 元数据的指针。

否则,它们的使用会带来性能损失,可能是
不可接受。 对我来说,上午 10 点 41 分,“乔什·布莱彻·斯奈德”<
通知@github.com> 写道:

通过选择建议,您可以选择让 ap 或 *p 给您更多
更好地控制内存权衡。

接口分配来存储标量值的原因是你不
必须阅读一个类型词才能确定另一个词是否是
指针; 见 #8405 https://github.com/golang/go/issues/8405
讨论。 相同的实施考虑可能适用于
选择类型,这可能意味着在实践中 p 最终分配并成为
反正非本地。


您收到此消息是因为您创作了该线程。
直接回复本邮件,在GitHub上查看
https://github.com/golang/go/issues/19412#issuecomment-323371837或静音
线程
https://github.com/notifications/unsubscribe-auth/AGGWB-wQD75N44TGoU6LWQhjED_uhKGUks5sZaKbgaJpZM4MTmSr
.

@urandom

当dummy方法被导出并且任何第三方都可以实现“sum类型”时会发生什么?

导出的方法和导出的类型是有区别的。 我们似乎在互相交谈。 对我来说,这似乎工作得很好,在开放式和封闭式总和之间没有任何区别:

type X interface { x() X }
type IntX int
func (v IntX) x() X { return v }
type StringX string
func (v StringX) x() X { return v }
type StructX struct{
    Foo bool
    Bar int
}
func (v StructX) x() X { return v }

包之外没有可能的扩展,但包的消费者可以像其他任何人一样使用、创建和传递值。

您可以在外部嵌入 X 或满足它的本地类型之一,然后将其传递给包中​​采用 X 的函数。

如果该 func 调用 x 它要么恐慌(如果 X 本身被嵌入且未设置为任何内容)或返回一个您的代码可以操作的值 - 但它不是调用者传递的值,这会让调用者感到有些惊讶(并且他们的代码已经怀疑他们是否正在尝试这样的事情,因为他们没有阅读文档)。

调用一个因“不要这样做”消息而恐慌的验证器似乎是处理该问题的最不令人惊讶的方法,并让调用者修复他们的代码。

如果该 func 调用 x 它要么会发生恐慌 [...] 要么返回一个您的代码可以对其进行操作的值 - 但它不是调用者传递的值,这会让调用者感到有些惊讶

就像我上面说的:如果您惊讶于您故意构建的无效值是无效的,您需要重新考虑您的期望。 但无论如何,这不是这种特殊讨论的内容,将不同的论点分开会很有帮助。 这个是关于@urandom说通过带有标签方法的接口的开放总和不会被其他包内省或使用。 我发现这是一个可疑的主张,如果能得到澄清就太好了。

问题是有人可以创建一个不在总和中的类型,该类型可以编译并传递给您的包。

如果不向语言添加适当的和类型,则有三种处理方式

  1. 无视情况
  2. 验证和恐慌/返回错误
  3. 尝试通过隐式提取嵌入值并使用它来“做你的意思”

3 对我来说似乎是 1 和 2 的奇怪组合:我看不出它买了什么。

我同意“如果您感到惊讶,您故意构建的无效值是无效的,您需要重新考虑您的期望”,但是,对于 3,很难注意到出现问题,即使您这样做了很难弄清楚为什么。

2 似乎是最好的,因为它既可以保护代码免于进入无效状态,又可以在有人搞砸时发出信号,让他们知道为什么出错以及如何纠正它。

我是误解了模式的意图,还是我们只是从不同的哲学来解决这个问题?

@urandom我也希望得到澄清; 我也不是 100% 确定你想说什么。

问题是有人可以创建一个不在总和中的类型,该类型可以编译并传递给您的包。

你总是可以这样做; 如果有疑问,您总是可以使用不安全的,即使使用编译器检查的和类型(我不认为这是从嵌入一些明显旨在作为和而不将其初始化为一个有效值)。 问题是“这在实践中多久会引起问题以及该问题有多严重”。 在我看来,上面的解决方案的答案是“几乎从不并且非常低”-您显然不同意,这很好。 但无论哪种方式,似乎都没有太多的意义在这一点上 - 这个特定点双方的论点和观点应该足够清楚,我试图避免太多嘈杂的重复并专注于真正的新的论点。 我提出上述结构是为了证明一流和类型和 emulated-sums-via-interfaces 之间的可导出性没有区别。 并不是要表明他们在各方面都严格。

如果有疑问,您总是可以使用不安全的,即使使用编译器检查的和类型(我不认为这是从嵌入一些明显旨在作为和而不将其初始化为一个有效值)。

我认为它在性质上是不同的:当人们以这种方式滥用嵌入时(至少使用proto.Message和实现它的具体类型),他们通常不会考虑它是否安全以及它可能会破坏哪些不变量. (用户假设接口完全描述了所需的行为,但是当接口被用作联合或和类型时,他们通常不会这样做。另见 https://github.com/golang/protobuf/issues/364。)

相比之下,如果有人使用包unsafe将变量设置为它通常无法引用的类型,他们或多或少明确声称至少考虑过他们可能会破坏的内容以及原因。

@Merovius也许我一直不清楚:编译器会告诉某人他们使用嵌入错误的事实更多是一个很好的附带好处。

安全特性的最大好处是它会被反射并以 go/types 表示。 这为工具和库提供了更多可使用的信息。 在 Go 中有很多方法可以模拟 sum 类型,但它们都与非 sum 类型代码相同,因此工具和库需要带外信息才能知道它是 sum 类型并且必须能够识别特定模式正在使用,但即使是这些模式也允许显着变化。

它还会使不安全成为创建无效值的唯一方法:现在您拥有常规代码、生成的代码和反射——后两者更有可能导致问题,因为与他们无法阅读文档的人不同。

安全性的另一个附带好处意味着编译器拥有更多信息并且可以生成更好更快的代码。

还有一个事实是,除了能够用接口替换伪和之外,您还可以替换伪和“这些常规类型之一”,如json.Tokendriver.Value 。 这些很少而且相距甚远,但需要interface{}地方会少一个。

它也会使不安全成为创建无效值的唯一方法

我认为我不理解导致此声明的“无效值”的定义。

@neild如果你有

var v pick {
  None struct{}
  A struct { X int; Y *T}
  B int
}

它会被布置在内存中

struct {
  activeField int //which of None (0), A (1), or B (2) is the current field
  theInt int // If None always 0
  thePtr *T // If None or B, always nil
}

和 unsafe 你可以设置thePtr即使activeField是 0 或 2 或者设置theInt的值,即使activeField是 0。

在任何一种情况下,这都会使编译器所做的假设无效,并允许我们今天可能遇到的相同类型的理论错误。

但正如@bcmills指出的那样,如果您使用的是不安全的,您最好知道自己在做什么,因为这是核选项。

我不明白的是为什么不安全是创建无效值的唯一方法。

var t time.Timer

t是无效值; t.C未设置,调用t.Stop会恐慌等。不需要 unsafe。

某些语言具有类型系统,它们竭尽全力防止创建“无效”值。 Go 不是其中之一。 我看不出工会如何显着地推动这根针。 (当然,还有其他理由支持工会。)

@neild是的,对不起,我对我的定义松散了。

对于 sum 类型的不变量,我应该说无效。

总和中的各个类型当然可以处于无效状态。

然而,维护 sum 类型不变量意味着它们可以被反射和 go/types 以及程序员访问,因此在库和工具中操作它们可以保持安全性并为元程序员提供更多信息

@jimmyfrasche ,我是说与 sum 类型不同,它告诉你它可以是所有可能的类型,接口是不透明的,因为你不知道,或者至少你不能使用,类型列表是什么实现接口的是。 这使得编写代码的switch部分有点猜测:

func F(sum SumInterface) {
    switch v := sum {
    case Screwdriver:
             ...
    default:
           panic ("Someone implementing a new type which gets passed to F and causes a runtime panic 3 weeks into production")
    }
}

因此,在我看来,人们在基于接口的和类型仿真中遇到的大多数问题都可以通过收费和/或约定来解决。 例如,如果一个接口包含一个未导出的方法,找出所有可能的(是的,有意规避)实现将是微不足道的。 同样,为了解决基于 iota 的枚举的大多数问题,“枚举是type Foo intconst ( FooA Foo = iota; FooB; FooC )形式的声明”的简单约定将能够编写广泛而精确的工具对他们也是。

是的,这并不等同于实际的总和类型(除其他外,它们不会获得一流的反射支持,尽管我并不真正了解这有多重要),但这确实意味着现有的解决方案从我的 POV 看来,比他们经常画的要好。 并且 IMO 在将它们实际放入 Go 2 之前探索该设计空间是值得的——至少如果它们对人们来说真的那么重要。

(我想再次强调,我知道和类型的优点,所以没有必要为了我的利益而重述它们。我只是没有像其他人那样权衡它们,也看到了缺点,因此对相同的数据得出不同的结论)

@Merovius这是一个很好的位置。

反射支持将允许库以及离线工具(linter、代码生成器等)访问信息并禁止它不适当地修改它,而这些修改不能以任何精度进行静态检测。

无论如何,探索它是一个公平的想法,所以让我们探索它。

总结一下 Go 中最常见的伪和系列是:(大致按出现顺序)

  • const/iota 枚举。
  • 与标签方法的接口,用于对同一包中定义的类型进行求和。
  • *T用于可选的T
  • 带有枚举的结构,其值决定了可以设置哪些字段(当枚举是 bool 并且只有一个其他字段时,这是另一种可选的T
  • interface{}仅限于一组有限类型的抓包。

所有这些都可以用于和类型和非和类型。 前两个很少用于其他任何事情,假设它们代表和类型并接受偶尔的误报可能是有意义的。 对于接口总和,它可以将其限制为没有参数或返回值且没有任何成员主体的未导出方法。 对于枚举,只有当它们只是Type = iota时才识别它们是有意义的,因此当 iota 用作表达式的一部分时它不会被绊倒。

*T对于可选的T真的很难与常规指针区分开来。 这可以给出约定type O = *T 。 这可能会被检测到,虽然有点困难,因为别名不是类型的一部分。 type O *T更容易检测,但更难在代码中使用。 另一方面,所有需要做的事情本质上都是内置在类型中的,因此通过识别这一点在工具中几乎没有什么好处。 让我们忽略这一点。 (泛型可能会允许类似于type Optional(T) *T ,这将简化“标记”这些)。

带有枚举的结构在工具中很难推理,哪些字段与枚举的哪个值对应? 我们可以将其简化为枚举中每个成员必须有一个字段并且枚举值和字段值必须相同的约定,例如:

type Which int
const (
  A Which = iota
  B
  C
)
type Sum struct {
  Which
  A struct{} // has to be included to line up with the value of Which
  B struct { X int; Y float64 }
  C struct { X int; Y int } 
}

那不会得到可选类型,但我们可以在识别器中特殊情况“2 个字段,第一个是 bool”。

如果没有像//gosum: int, float64, string, Foo这样的神奇注释,将无法检测到使用interface{}进行抓包金额

或者,可能有一个具有以下定义的特殊包:

package sum
type (
  Type struct{}
  Enum int
  OneOf interface{}
)

并且仅识别形式type MyEnum sum.Enum枚举,仅识别嵌入sum.Type接口和结构,并且仅识别interface{}抓包,如type GrabBag sum.OneOf (但这仍然需要机器可识别的注释来解释其注释)。 这将有以下优点和缺点:
优点

  • 代码中明确表示:如果如此标记,则它是 100% 的和类型,没有误报。
  • 这些定义可以有文档解释它们的含义,并且包文档可以链接到可以与这些类型一起使用的工具
  • 有些会在反射中具有一定的可见性
    缺点
  • 来自旧代码和 stdlib(不会使用它们)的大量误报。
  • 它们必须被用来有用,所以采用会很慢,并且可能永远不会达到 100%,并且识别这个特殊包的工具的有效性将是采用的函数,虽然实验很有趣,但可能不切实际。

无论使用这两种方法中的哪一种来识别和类型,让​​我们假设它们已被识别并继续使用该信息来查看我们可以构建什么样的工具。

我们可以粗略地将工具分为生成性(如 stringer)和内省性(如 golint)。

最简单的生成代码是一种工具,用于填充缺少案例的 switch 语句。 这可以被编辑器使用。 一旦 sum 类型被识别为 sum 类型,这是微不足道的(有点烦人,但实际的生成逻辑将是相同的,无论是否有语言支持)。

在所有情况下,都可以生成验证“其中之一”不变量的函数。

对于枚举,可能有更多的工具,如 stringer。 在https://github.com/golang/go/issues/19814#issuecomment -291002852 中,我提到了一些可能性。

最大的生成工具是编译器,它可以使用这些信息生成更好的机器代码,但是很好。

我暂时想不出其他人。 有人的愿望清单上有什么吗?

对于内省,明显的候选者是穷举性linting。 没有语言支持,实际上需要两种不同的 linting

  1. 确保处理所有可能的状态
  2. 确保没有创建无效状态(这会使 1 所做的工作无效)

1 是微不足道的,但它需要所有可能的状态和默认情况,因为 2 无法 100% 验证(甚至忽略不安全),而且您不能指望使用您的代码的所有代码无论如何都会运行此 linter。

2 不能真正通过反射或识别所有可能为总和生成无效状态的代码来跟踪值,但它可以捕获很多简单的错误,比如如果您嵌入一个总和类型然后用它调用一个 func,它可以说“您写了 pkg.F(v) 但您的意思是 pkg.F(v.EmbeddedField)”或“您将 2 传递给 pkg.F,使用 pkg.B”。 对于结构来说,它不能做太多来强制一次设置一个字段的不变性,除非在非常明显的情况下,例如“您正在切换哪个,并且在 X 的情况下,您将字段 F 设置为非零值”。 它可能会坚持要求您在接受来自包外部的值时使用生成的验证函数。

另一件大事将出现在 godoc 中。 godoc 已经将 const/iota 分组,#20131 将有助于接口伪和。 除了指定不变量之外,定义中没有明确的结构版本实际上没有任何关系。

以及离线工具——linter、代码生成器等。

不。静态信息存在,您不需要类型系统(或反射),约定工作正常。 如果您的接口包含未导出的方法,则任何静态工具都可以选择将其视为封闭和(因为它实际上是)并执行您可能想要的任何分析/代码生成。 同样与 iota 枚举的约定。

reflect 用于运行时类型信息 - 从某种意义上说,编译器删除了必要的信息,以使按约定求和在这里工作(因为它不让您访问函数列表或声明的类型或声明的常量),这这就是为什么我同意实际金额可以实现这一点。

(此外,FTR,根据用例,您仍然可以使用一个工具使用静态已知信息来生成必要的运行时信息 - 例如,它可以枚举具有所需标记方法的类型并生成查找表对他们来说。但我不明白用例是什么,所以很难评估它的实用性)。

所以,我的问题是故意的:在运行时提供这些信息的用例是什么?

无论如何,探索它是一个公平的想法,所以让我们探索它。

当我说“探索它”时,我的意思不是“列举它们并在真空中讨论它们”,我的意思是“实现使用这些约定的工具,看看它们有多么有用/必要/实用”。

经验报告的优势在于,它们基于经验:你需要做一件事,你试图使用现有的机制来做,你发现它们不够。 这将讨论集中在实际用例上(如在“使用它的情况”中),并能够评估针对它们的任何提议的解决方案,针对尝试过的替代方案,并查看解决方案如何不会有相同的陷阱。

您正在跳过“尝试使用现有机制”部分。 您希望对总和(问题)进行静态详尽检查。 编写一个工具来查找具有未导出方法的接口,对其使用的任何类型开关进行详尽检查,使用该工具一段时间(使用现有机制)。 写下来,失败的地方。

我在大声思考并开始基于工具可能使用的那些想法开发静态识别器。 我想,我是在暗中寻找反馈和更多想法(并且重新生成反映所需的信息是有回报的)。

FWIW,如果我在那里你我会简单地忽略复杂的情况并专注于工作的事情:a)接口中未导出的方法和b)简单的const-iota-enums,将int作为底层类型和单个const-预期格式的声明。 使用工具需要使用这两种解决方法之一,但 IMO 很好(要使用编译器工具,您还需要明确使用 sums,所以这看起来没问题)。

这绝对是一个很好的起点,它可以在运行大量软件包并查看有多少误报/否定后拨入

https://godoc.org/github.com/jimmyfrasche/closed

仍在进行中。 我不能保证我不必在构造函数中添加额外的参数。 它可能有比测试更多的错误。 不过玩玩就够了。

在 cmds/closed-exporer 中有一个使用示例,它还将列出在其导入路径指定的包中检测到的所有关闭类型。

我开始只是用未导出的方法检测所有接口,但它们相当普遍,虽然有些显然是和类型,但有些显然不是。 如果我只是将其限制为空标记方法约定,我会丢失很多 sum 类型,因此我决定分别记录两者并将包概括为超出 sum 类型的一些封闭类型。

对于枚举,我走了另一条路,只记录了定义类型的每个非位集常量。 我也计划公开发现的位集。

它不会检测可选结构或定义的空接口,因为它们需要某种标记注释,但它会处理 stdlib 中的特殊情况。

我开始只是用未导出的方法检测所有接口,但它们相当普遍,虽然有些显然是和类型,但有些显然不是。

如果你能提供一些没有的例子,我会发现它很有帮助。

@Merovius抱歉我没有保留清单。 我通过运行 stdlib.sh(在 cmds/closed-explorer 中)找到了它们。 如果我下次玩这个时遇到一个很好的例子,我会发布它。

我没有考虑作为 sum 类型的那些都是未导出的接口,它们被用来插入几种实现之一:没有人关心接口中的内容,只是有一些东西可以满足它。 它们非常多地被用作接口而不是总和,但恰好因为它们未导出而被关闭。 也许这是一个没有区别的区别,但我可以在进一步调查后改变主意。

@jimmyfrasche我认为这些应该被适当地视为封闭的总和。 我认为,如果他们不关心动态类型(即只调用接口中的方法),那么静态 linter 就不会抱怨,因为“所有开关都是详尽的”——所以对待它们没有缺点作为封闭的总和。 如果OTOH,有时也会型开关和遗漏的情况下,抱怨是正确的-这将恰好是那种的棉短绒应该抓的事情。

我想用一个好词来探索联合类型如何减少内存使用。 我正在用 Go 编写一个解释器,并且有一个必须作为接口实现的 Value 类型,因为 Values 可以是指向不同类型的指针。 这大概意味着一个 []Value 占用的内存是在 C 中用一个小标签封装指针的两倍。它看起来很多吗?

语言规范不需要提及这一点,但对于某些小型联合类型,将数组的内存使用量减少一半似乎是联合的一个非常有说服力的论据? 它可以让你做一些据我所知在今天的 Go 中不可能做的事情。 相比之下,在接口之上实现联合可以帮助提高程序的正确性和可理解性,但在机器级别并没有做任何新的事情。

我没有做过任何性能测试; 只是指出了一个研究方向。

您可以将 Value 实现为 unsafe.Pointer。

2018 年 2 月 6 日下午 3:54,“Brian Slesinsky”通知@github.com 写道:

我想用一个好词来探索联合类型如何减少
内存使用情况。 我正在用 Go 编写一个解释器,并且有一个 Value 类型
必须实现为接口,因为值可以是指针
到不同类型。 这大概意味着 [] 值占用两倍
内存与像你可以做的那样用一个小的位标签打包指针
在 C. 似乎很多?

语言规范不用提这个,不过好像要砍内存
对于一些小的联合类型,将数组对半使用可能很不错
工会的有力论据? 它可以让你做一些就我而言
知道今天在 Go 中是不可能做到的。 相比之下,在
接口顶部可以帮助程序的正确性和
可理解性,但在机器级别没有做任何新的事情。

我没有做过任何性能测试; 只是指出一个方向
研究。


你收到这个是因为你被提到了。
直接回复本邮件,在GitHub上查看
https://github.com/golang/go/issues/19412#issuecomment-363561070或静音
线程
https://github.com/notifications/unsubscribe-auth/AGGWBz-L3t0YosVIJmYNyf2iQ-YgIXLGks5tSLv9gaJpZM4MTmSr
.

@skybrian关于和类型的实现,这似乎很

这给您留下了:和类型可能会被标记为联合,并且可能会像现在一样占用切片中的空间。 除非切片是同质的,但您现在也可以使用更具体的切片类型。

是的。 在非常特殊的情况下,如果您专门针对它们进行优化,您可能可以节省一些内存,但如果您确实需要,您似乎也可以为此手动优化。

@DemiMarie unsafe.Pointer 在 App Engine 上不起作用,无论如何,它不会让您在不弄乱垃圾收集器的情况下打包位。 即使有可能,它也不是便携式的。

@Merovius是的,它确实需要更改运行时和垃圾收集器以了解压缩内存布局。 这就是重点; 指针是由 Go 运行时管理的,所以如果你想以一种安全的方式比接口做得更好,你不能在库或编译器中做到这一点。

但我会欣然承认,编写快速解释器是一个不常见的用例。 也许还有其他人? 似乎激发语言功能的一个好方法是找到当今无法在 Go 中轻松完成的事情。

那是真实的。

我的想法是 Go 不是编写解释器的最佳语言,
由于此类软件的动态性很强。 如果您需要高性能,
你的热循环应该用汇编编写。 你有什么理由吗
需要编写一个适用于 App Engine 的解释器吗?

2018 年 2 月 6 日下午 6:15,“Brian Slesinsky”通知@github.com 写道:

@DemiMarie https://github.com/demimarie unsafe.Pointer 在应用程序上不起作用
引擎,无论如何,它不会让你在没有
搞乱垃圾收集器。 就算有可能也不会
便携的。

@metrovius是的,它确实需要更改运行时和垃圾收集器
了解压缩内存布局。 这就是重点; 指针是
由 Go 运行时管理,所以如果你想做得比接口更好
安全的方式,您不能在库或编译器中执行此操作。

但我会欣然承认,编写快速解释器是一种不常见的用途
案件。 也许还有其他人? 这似乎是激励一个人的好方法
语言功能是找到今天在 Go 中不容易完成的事情。


你收到这个是因为你被提到了。
直接回复本邮件,在GitHub上查看
https://github.com/golang/go/issues/19412#issuecomment-363598572或静音
线程
https://github.com/notifications/unsubscribe-auth/AGGWB65jRKg_qVPWTiq8LbGk3YM1RUasks5tSN0tgaJpZM4MTmSr
.

我发现@rogpeppe 的提议非常吸引人。 我还想知道是否有可能解锁额外的好处,以配合@griesemer 已经确定的那些好处。

提案说:“和类型的方法集持有方法集的交集
它的所有组件类型,不包括任何具有相同
名字,但签名不同。”。

但是类型不仅仅是一个方法集。 如果 sum 类型支持其组件类型支持的操作的交集怎么办?

例如,考虑:

var x int|float64

这个想法是以下内容会起作用。

x += 5

这相当于写出完整的类型开关:

switch i := x.(type) {
case int:
    x = i + 5
case float64:
    x = i + 5
}

另一种变体涉及类型开关,其中组件类型本身就是和类型。

type Num int | float64
type StringOrNum string | Num 
var x StringOrNum

switch i := x.(type) {
case string:
    // Do string stuff.
case Num:
    // Would be nice if we could use i as a Num here.
}

此外,我认为 sum 类型和使用类型约束的泛型系统之间可能存在非常好的协同作用。

var x int|float64

var x, y int | float64呢? 添加这些时,这里的规则是什么? 进行了哪种有损转换(以及为什么)? 结果类型是什么?

Go 不会故意在表达式中进行自动转换(就像 C 那样)——这些问题不容易回答,而且会导致错误。

更有趣的是:

var x, y, z int|string|rune
x = 42
y = 'a'
z = "b"
fmt.Println(x + y + z)
fmt.Println(x + z + y)
fmt.Println(y + x + z)
fmt.Println(y + z + x)
fmt.Println(z + x + y)
fmt.Println(z + y + x)

所有intstringrune都有一个+操作符; 上面的打印是什么,为什么,最重要的是,结果怎么能完全混乱?

var x, y int | float64呢? 添加这些时,这里的规则是什么? 进行了哪种有损转换(以及为什么)? 结果类型是什么?

@Merovius没有隐含地进行有损转换,尽管我可以看到我的措辞如何给人以抱歉的印象。 在这里,一个简单的x + y不会编译,因为它意味着可能的隐式转换。 但是以下任何一个都可以编译:

z = int(x) + int(y)
z = float64(x) + float64(y)

同样,您的 xyz 示例无法编译,因为它需要可能的隐式转换。

我认为“支持支持的操作的交集”听起来不错,但并不能完全传达我的意图。 添加诸如“为所有组件类型编译”之类的内容有助于描述我认为它是如何工作的。

另一个例子是如果所有组件类型都是切片和映射。 能够在 sum 类型上调用 len 而无需类型切换会很好。

所有 int、string 和 rune 都有一个 + 运算符; 上面的打印是什么,为什么,最重要的是,结果怎么能不完全混乱?

只是想补充一下我的“如果和类型支持其组件类型支持的操作的交集怎么办?” 受到 Go Spec 对类型的描述的启发,即“类型确定一组值以及特定于这些值的操作和方法。”。

我试图说明的一点是,类型不仅仅是值和方法,因此 sum 类型可以尝试从其组件类型中捕获其他内容的共性。 这个“其他东西”比一组运算符更微妙。

另一个例子是与 nil 的比较:

var x []int | []string
fmt.Println(x == nil)  // Prints true
x = []string(nil)
fmt.Println(x == nil)  // Still prints true

两种组件类型都至少有一种类型可与 nil 相媲美,因此我们允许将 sum 类型与 nil 进行比较,而无需类型切换。 当然,这与接口当前的行为方式有些不一致,但根据https://github.com/golang/go/issues/22729 ,这可能不是一件坏事

编辑:平等测试在这里是一个不好的例子,因为我认为它应该更宽容,并且只需要一个或多个组件类型的潜在匹配。 反映了这方面的分配。

问题是,结果要么 a) 具有与自动转换相同的问题,要么 b) 范围极其有限(并且 IMO 令人困惑) - 即,所有运算符最多只能使用无类型文字。

我还有另一个问题,那就是允许它甚至会进一步限制它们对组成类型演变的鲁棒性——现在你可以在保持向后兼容性的同时添加的唯一类型是允许其组成类型的所有操作的类型。

所有这一切对我来说似乎真的很混乱,只是为了一个非常小的(如果有的话)有形的好处。

现在,您可以在保持向后兼容性的同时添加的唯一类型是允许其组成类型的所有操作的类型。

哦,还要明确说明这一点:它意味着您永远无法决定是否要扩展参数或返回类型或变量,或者……从单例类型到总和。 因为添加任何新类型都会使某些操作(如赋值)无法 compile.l

@Merovius注意,原始提案中已经存在兼容性问题的一个变体,因为“和类型的方法集持有方法集的交集
的所有组件类型”。因此,如果您添加未实现该方法集的新组件类型,那么这将是一个非向后兼容的更改。

哦,还要明确说明这一点:它意味着您永远无法决定是否要扩展参数或返回类型或变量,或者……从单例类型到总和。 因为添加任何新类型都会使某些操作(如赋值)无法 compile.l

分配行为将保持如@rogpeppe所描述的

如果不出意外,我认为需要澄清原始 rogpeppe 提案关于类型开关之外的 sum 类型的行为。 包括赋值和方法集,但仅此而已。 平等呢? 我认为我们可以做得比 interface{} 做得更好:

var x int | float64
fmt.Println(x == "hello")  // compilation error?
x = 0.0
fmt.Println(x == 0) // true or false?  I vote true :-)

因此,如果您添加未实现该方法集的新组件类型,那么这将是非向后兼容的更改。

你总是可以添加方法,但你不能重载运算符来处理新类型。 这正是不同之处——在他们的提议中,你只能在 sum-value 上调用通用方法(或分配给它),除非你用 type-assertion/-switch 解包它。 因此,只要您添加的类型具有必要的方法,就不会发生重大变化。 在您的提案中,它仍然是一个重大更改,因为用户可能会使用您无法重载的运算符。

(您可能想指出,向总和添加类型仍然是一个重大变化,因为类型开关不会在其中包含新类型。这正是我也不赞成原始提案的原因 - 我不想因为这个原因而关闭总和)

分配行为将保持如@rogpeppe 所述

他们的提议只讨论了总和值的分配,我讨论了总和值(到其组成部分之一)的分配。 我同意他们的提案也不允许这样做,但不同的是,他们的提案并不是要增加这种可能性。 即我的论点正是,您建议的语义并不是特别有益,因为在实践中,它们的使用受到严重限制。

fmt.Println(x == "hello") // compilation error?

这也可能会添加到他们的提案中。 我们已经有了一个等效的接口特例,即

当类型 X 的值具有可比性且 X 实现 T 时,非接口类型 X 的值 x 和接口类型 T 的值 t 具有可比性。如果 t 的动态类型与 X 相同且 t 的动态值等于 x,则它们相等.

fmt.Println(x == 0) // true or false? I vote true :-)

估计是假的。 鉴于,类似

var x int|float64 = 0.0
y := 0
fmt.Println(x == y)

应该是一个编译错误(正如我们在上面得出的结论),这个问题只有在与无类型的数字常量比较时才真正有意义。 在这一点上,这取决于如何将其添加到规范中。 您可能会争辩说,这类似于将常量分配给接口类型,因此它应该具有其默认类型(然后比较结果为假)。 哪个 IMO 非常好,我们今天已经

然而,无论以哪种方式回答这个问题,都不需要允许所有表达式都使用可能对组成部分有意义的和类型。

但要重申的是:我不赞成对金额提出不同的建议。 我反对这个。

fmt.Println(x == "hello") // compilation error?

这也可能会添加到他们的提案中。

更正:规范已经涵盖了这个编译错误,因为它包含了语句

在任何比较中,第一个操作数必须可分配给第二个操作数的类型,反之亦然。

@Merovius您对我的提案变体提出了一些好观点。 我将避免进一步辩论它们,但我想进一步深入探讨与 0 问题的比较,因为它同样适用于原始提案。

fmt.Println(x == 0) // true or false? I vote true :-)

估计是假的。 鉴于,类似

var x int|float64 = 0.0
y := 0
fmt.Println(x == y)
应该是一个编译错误(正如我们上面总结的那样),

我不觉得这个例子很有说服力,因为如果你将第一行更改为var x float64 = 0.0那么你可以使用相同的推理来论证将 float64 与 0 进行比较应该是错误的。 (次要要点:(a)我假设您的意思是第一行的 float64(0) ,因为 0.0 可分配给 int。(b) x==y 在您的示例中不应该是编译错误。不过它应该打印 false。)

我认为你的想法“这类似于为接口类型分配一个常量,因此它应该有它的默认类型”更引人注目(假设你的意思是 sum 类型),所以这个例子是:

var x,y int|float64 = float64(0), 0
fmt.Println(x == y) // 假

我仍然认为x == 0应该是真的。 我的心智模型是尽可能晚地将类型赋予 0。 我意识到这与接口的当前行为相反,这正是我提出它的原因。 我同意这并没有导致“太多的模糊”,但是将接口与 nil 进行比较的类似问题导致了很多混乱。 我相信,如果 sum 类型出现并保留旧的相等语义,我们会看到与 0 相比类似的混乱程度。

我不觉得这个例子很有说服力,因为如果你将第一行更改为 var x float64 = 0.0 那么你可以使用相同的推理来论证将 float64 与 0 进行比较应该是假的。

我没有说应该,我说大概,考虑到我认为在如何实施他们的提案的简单性/有用性之间最有可能的权衡。 我并不是要做出价值判断。 事实上,如果使用同样简单的规则我们可以让它打印为真,我可能会更喜欢它。 我只是不乐观。

请注意,将float64(0)int(0) (即用var x float64 = 0.0替换总和的示例)不是false ,但是,它是一个编译时错误(应该如此)。 这正是我的观点; 您的建议仅在与无类型常量结合使用时才真正有用,因为对于其他任何内容,它都无法编译。

(a) 我假设您的意思是第一行的 float64(0),因为 0.0 可分配给 int。

当然(我假设语义更接近常量表达式的当前“默认类型”,但我同意当前的措辞并不暗示这一点)。

(b) x==y 在您的示例中不应该是编译错误。 不过它应该打印 false。)

不,应该是编译时错误。 您已经说过,当且仅当表达式可以使用任何选择的成分类型进行编译时,才应该允许操作e1 == y ,其中e1是和类型表达式。 鉴于在我的示例中, x类型int|float64并且y类型int并给出了float64int没有可比性,显然违反了这个条件。

要进行此编译,您需要删除替换任何组成类型表达式也需要编译的条件; 在这一点上,我们必须设置规则在这些表达式中使用时如何提升或转换类型(也称为“C 混乱”)。

过去的共识是 sum 类型不会对接口类型增加太多。

它们确实不适用于 Go 的大多数用例:琐碎的网络服务和实用程序。 但是一旦系统变得更大,它们很有可能是有用的。
我目前正在编写一个重度分布式服务,通过大量逻辑实现数据一致性保证,我进入了它们会很方便的情况。 随着服务变得越来越大,这些 NPD 变得太烦人了,我们看不到拆分它的明智方法。
我的意思是 Go 的类型系统保证对于比典型的原始网络服务更复杂的东西来说有点太弱了。

但是,Rust 的故事表明,像在 Haskell 中一样使用和类型进行 NPD 和错误处理是一个坏主意:有典型的自然命令式工作流,而 Haskellish 方法并不适合它。

例子

考虑伪代码中的iotuils.WriteFile类函数。 命令式流程看起来像这样

file = open(name, os.write)
if file is error
    return error("cannot open " + name + " writing: " + file.error)
if file.write(data) is error:
    return error("cannot write into " + name + " : " + file.error)
return ok

以及它在 Rust 中的样子

match open(name, os.write)
    file
        match file.write(data, os.write)
            err
                return error("cannot open " + name + " writing: " + err)
            ok
                return ok
    err
        return error("cannot write into " + name + " : " + err)

它安全但丑陋。

还有我的提议:

type result[T, Err] oneof {
    default T
    Error Err
}

以及程序的外观( result[void, string] = !void

file := os.Open(name, ...)
if !file {
    return result.Error("cannot open " + name + " writing: " + file.Error)
}
if res := file.Write(data); !res {
    return result.Error("cannot write into " + name + " : " + res.Error)
}
return ok

这里默认分支是匿名的,错误分支可以用.Error (一旦知道结果是错误)。 一旦知道文件已成功打开,用户就可以通过变量本身访问它。 首先,如果我们确保file成功打开或退出(因此进一步的语句知道该文件不是错误)。

如您所见,这种方法保留了命令式流并提供了类型安全。 NPD 处理可以通过类似的方式完成:

type Reference[T] oneof {
    default T
    nil
}
// Reference[T] = *T

处理类似于结果

@sirkon ,你的 Rust 示例并没有让我相信像 Rust 中的简单和类型有什么问题。 相反,它表明可以使用if语句使和类型的模式匹配更像 Go。 就像是:

ferr := os.Open(name, ...)
if err(e) := ferr {           // conditional match and unpack, initializing e
  return fmt.Errorf("cannot open %v: %v", name, e)
}
ok(f) := ferr                  // unconditional match and unpack, initializing f
werr := f.Write(data)
...

(本着 sum 类型的精神,如果编译器不能证明无条件匹配总是成功,因为只剩下一个 case,那将是编译错误。)

对于基本的错误检查,这似乎不是对多个返回值的改进,因为它长了一行并声明了一个更多的局部变量。 但是,它可以更好地扩展到多个案例(通过添加更多 if 语句),并且编译器可以检查是否处理了所有案例。

@sirkon

它们确实不适用于 Go 的大多数用例:琐碎的网络服务和实用程序。 但是一旦系统变得更大,它们很有可能是有用的。
[…]
我的意思是 Go 的类型系统保证对于比典型的原始网络服务更复杂的东西来说有点太弱了。

像这样的声明是不必要的对抗和贬义。 他们也有点尴尬,TBH,因为有非常大的、非常重要的服务是用 Go 编写的。 考虑到它的大部分开发人员在 Google 工作,你应该假设他们比你更了解,如果它适合编写大型和非平凡的服务。 Go 可能不会涵盖所有用例(IMO 也不应该),但从经验上讲,它不仅适用于“原始网络服务”。

NPD 处理可以用类似的方式完成

我认为这确实说明您的方法实际上并没有增加任何重要的价值。 正如您所指出的,它只是为取消引用添加了不同的语法。 但是 AFAICT 没有什么可以阻止程序员在 nil 值上使用该语法(这可能仍然会引起恐慌)。 即每个使用*p有效的程序也使用p.T (或者它是p.default ?很难说出你的想法具体什么),反之亦然。

sum 类型可以添加到错误处理和 nil-dereferences 的一个优点是编译器可以强制您必须通过模式匹配来证明该操作是安全的。 省略了执法似乎表带来显著新的东西,不的提案(可以说,它比通过接口采用开放的款项更差),而没有包括它的建议是什么,你形容为“丑陋”。

@Merovius

考虑到它的大部分开发人员在谷歌工作,你应该假设他们比你更了解,

信徒有福了。

正如您所指出的,它只是为取消引用添加了不同的语法。

再次

var written int64
...
res := os.Stdout.Write(data) // Write([]byte) -> Result[int64, string] ≈ !int64
written += res // Will not compile as res is a packed result type
if !res {
    // we are living on non-default res branch thus the only choice left is the default
    return Result.Error(...)
}
written += res // is OK

@skybrian

ferr := os.Open(...)

这个中间变量迫使我放弃这个想法。 如您所见,我的方法专门用于错误和零处理。 这些微小的任务太重要了,值得 IMO 特别关注。

@sirkon您显然对与人

让我们保持我们的对话文明,避免非建设性的评论。 我们可以在某些事情上有不同意见,但仍然保持体面的话语。 https://golang.org/conduct。

考虑到它的大部分开发人员在谷歌工作,你应该假设他们比你更了解

我怀疑你能在谷歌提出这样的论点。

@hasufell那个家伙来自德国,在那里他们没有大型 IT 公司进行垃圾面试来提升面试官的自负和庞然大物的管理,这就是为什么

@sirkon同样适用于您。 Ad-hominem 和社会争论是没有用的。 这不仅仅是一个 CoC 问题。 我已经看到这种关于核心语言的“社会争论”经常出现:编译器开发人员更了解,语言设计人员更了解,谷歌人员更了解。

不,他们没有。 没有知识权威。 只有决策权。 克服它。

隐藏一些评论以重置对话(并感谢@agnivade试图让它回到

伙计们,请根据我们的Gopher 价值观考虑您在这些讨论中的角色:社区中的每个人都有自己的观点,我们应该努力在我们如何解释和回应彼此时保持尊重和慈善。

请允许我在此讨论中添加我的 2 美分:

我们需要一种方法将不同类型按方法集以外的功能组合在一起(如接口)。 新的分组功能应该允许将没有任何方法的原始(或基本)类型和接口类型归类为相关相似。 我们可以按原样保留原始类型(布尔值、数字、字符串,甚至 []byte、[]int 等),但可以从类型之间的差异中抽象出来,其中类型定义将它们分组为一个系列。

我建议我们在语言中添加类似类型 _family_ 构造的东西。

语法

一个类型族可以像任何其他类型一样定义:

type theFamilyName family {
    someType
    anotherType
}

正式的语法类似于:
FamilyType = "family" "{" { TypeName ";" } "}" .

类型族可以在函数签名中定义:

func Display(s family{string; fmt.Stringer}) { /* function body */ }

也就是说,单行定义需要在类型名称之间使用分号。

家族类型的零值是 nil,就像 nil 接口一样。

(在幕后,家族抽象背后的值的实现很像一个接口。)

推理

我们需要比空接口更精确的东西,在空接口中我们要指定哪些类型作为函数的参数或函数的返回是有效的。

提议的解决方案将实现更好的类型安全性,在编译时进行全面检查,并且在运行时不会增加额外的开销。

关键是_Go 代码应该更加自我记录_。 函数可以作为参数的内容应该内置到代码本身中。

太多代码错误地利用了“接口{}什么都不说”这一事实。 _nothing_ 说,Go 中如此广泛使用(和滥用)的结构有点令人尴尬,没有它我们将无法做很多事情。

一些例子

sql.Rows.Scan函数的文档包含一个大块,详细说明了可以传入函数的类型:

Scan converts columns read from the database into the following common Go types and special types provided by the sql package:
 *string
 *[]byte
 *int, *int8, *int16, *int32, *int64
 *uint, *uint8, *uint16, *uint32, *uint64
 *bool
 *float32, *float64
 *interface{}
 *RawBytes
 any type implementing Scanner (see Scanner docs)

对于sql.Row.Scan函数,文档中包含一句话“有关详细信息,请参阅 Rows.Scan 上的文档”。 有关详细信息,请参阅_some other function_的文档? 这不是 Go-like——在这种情况下,这句话是不正确的,因为实际上Rows.Scan可以接受*RawBytes值,但Row.Scan不能。

问题是我们经常被迫依赖于保证和行为契约的注释,而编译器无法强制执行这些注释。

当一个函数的文档说这个函数的工作方式和其他函数一样——“所以去看看那个其他函数的文档”——你几乎可以保证这个函数有时会被误用。 我敢打赌,大多数人都和我一样,只发现了一个*RawBytes不允许在参数Row.Scan只获得了从错误之后的Row.Scan (说“sql:Row.Scan 上不允许使用 RawBytes”)。 令人遗憾的是,类型系统允许这样的错误。

我们可以改为:

type Value family {
    *string
    *[]byte
    *int; *int8; *int16; *int32; *int64
    *uint; *uint8; *uint16; *uint32; *uint64
    *bool
    *float32; *float64
    *interface{}
    *RawBytes
    Scanner
}

这样,传入的值必须是给定族中的类型之一,并且Rows.Scan函数内部的类型切换将不需要处理任何意外或默认情况; Row.Scan函数会有另一个系列。

还要考虑cloud.google.com/go/datastore.Property结构如何具有interface{}类型的“值”字段并需要所有这些文档:

// Value is the property value. The valid types are:
// - int64
// - bool
// - string
// - float64
// - *Key
// - time.Time
// - GeoPoint
// - []byte (up to 1 megabyte in length)
// - *Entity (representing a nested struct)
// Value can also be:
// - []interface{} where each element is one of the above types
// This set is smaller than the set of valid struct field types that the
// datastore can load and save. A Value's type must be explicitly on
// the list above; it is not sufficient for the underlying type to be
// on that list. For example, a Value of "type myInt64 int64" is
// invalid. Smaller-width integers and floats are also invalid. Again,
// this is more restrictive than the set of valid struct field types.
//
// A Value will have an opaque type when loading entities from an index,
// such as via a projection query. Load entities into a struct instead
// of a PropertyLoadSaver when using a projection query.
//
// A Value may also be the nil interface value; this is equivalent to
// Python's None but not directly representable by a Go struct. Loading
// a nil-valued property into a struct will set that field to the zero
// value.

这可能是:

type PropertyVal family {
  int64
  bool
  string
  float64
  *Key
  time.Time
  GeoPoint
  []byte
  *Entity
  nil
  []int64; []bool; []string; []float64; []*Key; []time.Time; []GeoPoint; [][]byte; []*Entity
}

(您可以想象如何将其更清洁地分成两个家庭。)

上面提到了json.Token类型。 它的类型定义是:

type Token family {
    Delim
    bool
    float64
    Number
    string
    nil
}

我最近听到的另一个例子:
当调用像sql.DB.Execsql.DB.Query函数,或任何采用interface{}可变参数列表的函数时,其中每个元素都必须在特定集合中具有类型,并且 _not 本身是slice_,重要的是要记住在将[]interface{}中的参数传入这样的函数时使用“spread”运算符:说DB.Exec("some query with placeholders", emptyInterfaceSlice)是错误的; 正确的方法是: DB.Exec("the query...", emptyInterfaceSlice...)其中emptyInterfaceSlice类型[]interface{} 。 避免此类错误的一种优雅方法是让此函数采用Value的可变参数参数,其中Value被定义为如上所述的族。

这些例子的重点是_真正的错误正在发生_,因为interface{}的不精确。

var x int | float64 | string | rune
z = int(x) + int(y)
z = float64(x) + float64(y)

这绝对应该是编译器错误,因为x的类型与可以传递给int()的类型并不真正兼容。

我喜欢有family的想法。 它本质上是一个对列出的类型进行约束(受限?)的接口,编译器可以确保您始终匹配并在相应的case的本地上下文中更改变量的类型。

问题是我们经常被迫依赖评论来保证和
行为契约,编译器无法强制执行。

这实际上是我开始有点不喜欢这样的东西的原因

func foo() (..., error) 

因为你不知道它返回什么样的错误。

以及其他一些返回接口而不是具体类型的东西。 部分功能
return net.Addr有时很难深入挖掘源代码以找出它实际返回的net.Addr ,然后适当地使用它。 返回具体类型并没有太大的缺点(因为它实现了接口,因此可以在可以使用接口的任何地方使用),除非您
稍后计划扩展您的方法以返回不同类型的net.Addr 。 但是如果你的
API 提到它返回OpError那么为什么不把那部分作为“编译时”规范的一部分呢?

例如:

 OpError is the error type usually returned by functions in the net package. It describes the operation, network type, and address of an error. 

通常? 不会准确地告诉您哪些函数会返回此错误。 这是类型的文档,而不是函数。 Read的文档没有提到它返回 OpError。 另外,如果你这样做

err := blabla.(*OpError)

一旦返回不同类型的错误,它就会崩溃。 这就是为什么我真的希望将其视为函数声明的一部分。 至少*OpError | error会告诉你它会返回
这样的错误,编译器会确保您将来不会执行未经检查的类型断言使您的程序崩溃。

顺便说一句:像 Haskell 的类型多态性这样的系统是否被考虑过? 或基于“特征”的类型系统,即:

func calc(a < add(a, a) a >, b a) a {
   return add(a, b)
}

func drawWidgets(widgets []< widgets.draw() error >) error {
  for _, widgets := range widgets {
    err := widgets.draw()
    if err != nil {
      return err
    }
  }
  return nil
}

a < add(a, a) a意思是“无论 a 的类型是什么,都必须存在一个函数 add(typeof a, typeof a) typeof a)”。 < widgets.draw() error>表示“无论小部件的类型如何,它都必须提供一个返回错误的方法 draw”。 这将允许创建更多通用函数:

func Sum(a []< add(a,a) a >) a {
  sum := a[0]
  for i := 1; i < len(a); i++ {
    sum = add(sum,a[i])
  }
  return sum
}

(请注意,这不等于传统的“泛型”)。

返回具体类型并没有太大的缺点(因为它实现了接口,因此可以在可以使用该接口的任何地方使用),除非您以后计划扩展您的方法以返回不同类型的net.Addr .

此外,Go 没有变体子类型,因此您不能在需要时将func() *FooError用作func() error 。 这对于界面满意度尤其重要。 最后,这不能编译:

func Foo() (FooVal, FooError) {
    // ...
}

func Bar(f FooVal) (BarVal, BarError) {
    // ...
}

func main() {
    foo, err := Foo()
    if err != nil {
        log.Fatal(err)
    }
    bar, err := Bar(foo) // Type error: Can not assign BarError to err (type FooError)
    if err != nil {
        log.Fatal(err)
    }
}

即,使这项工作(我想,如果我们能以某种方式),我们就需要更为复杂的类型推断-目前,只去使用从单一的表达本地类型的信息。 以我的经验,这些类型的类型推断算法不仅显着变慢(减慢编译速度,通常甚至没有限制运行时),而且产生的错误消息也很难理解。

此外,Go 没有变体子类型,因此您不能在需要时使用 func() *FooError 作为 func() 错误。 这对于界面满意度尤其重要。 最后,这不能编译:

我原以为这在 Go 中运行良好,但我从未偶然发现这一点,因为目前的做法是仅使用error 。 但是是的,在这种情况下,这些限制实际上迫使您使用error作为返回类型。

func main() {
    foo, err := Foo()
    if err != nil {
        log.Fatal(err)
    }
    bar, err := Bar(foo) // Type error: Can not assign BarError to err (type FooError)
    if err != nil {
        log.Fatal(err)
    }
}

我不知道任何允许这样做的语言(好吧,除了 esolangs),但您要做的就是保留一个“类型世界”(基本上是variable -> type的地图),如果您重新-分配您刚刚在“类型世界”中更新其类型的变量。

我不认为你需要复杂的类型推断来做到这一点,但你需要跟踪变量的类型,但我假设你无论如何都需要这样做,因为

var int i = 0;
i = "hi";

您肯定必须以某种方式记住哪些变量/声明具有哪些类型,对于i = "hi"您需要对i进行“类型查找”以检查是否可以为其分配字符串。

是否有实际问题复杂化的是分配func () *ConcreteErrorfunc() error不是不支持它(如运行原因/编译代码的原因)类型检查其他? 我想目前你必须将它包装在这样的函数中:

type MyFunc func() error

type A struct {
}

func (_ *A) Error() string { return "" }

func NewA() *A {
    return &A{}
}

func main() {
    var err error = &A{}
    fmt.Println(err.Error())
    var mf MyFunc = MyFunc(func() error { return NewA() }) // type checks fine
        //var mf MyFunc = MyFunc(NewA) // doesn't type check
    _ = mf
}

如果您遇到func (a, b) c但得到func (x, y) z ,则需要做的就是检查z是否可分配给c (和ab必须可以分配给xy )至少在类型级别不涉及复杂的类型推断(它只涉及检查是否一种类型可分配/兼容/兼容另一种类型)。 当然,这是否会导致运行时/编译问题......我不知道,但至少严格查看类型级别我不明白为什么这会涉及复杂的类型推断。 类型检查器已经知道x可以分配给a因此它也很容易知道func () x可以分配给func () a 。 当然,可能有实际原因(考虑运行时表示)为什么这不容易实现。 (我怀疑这是真正的症结所在,而不是实际的类型检查)。

从理论上讲,您可以使用自动包装函数(如上面的代码片段)来解决运行时问题(如果有),但它的缺点是它会破坏 funcs 与 funcs 的比较(因为被包装的 func 将不等于 func它包装)。

我不知道任何允许这样做的语言(嗯,除了 esolangs)

不完全是,但我认为这是因为具有强大类型系统的语言通常是不真正使用变量的函数式语言(因此并不真正需要重用标识符的能力)。 FWIW,我认为例如 Haskell 的类型系统能够很好地处理这个问题 - 至少只要你不使用FooErrorBarError任何其他属性,它应该能够推断出errerror并处理它。 当然,这也是一个假设,因为这种确切的情况不容易转移到函数式语言中。

但我假设你无论如何都需要这样做,因为

不同之处在于,在您的示例中, i在第一行之后有一个清晰易懂的类型,即int ,然后在分配string时遇到类型错误

是否有实际问题复杂化的是分配func () *ConcreteErrorfunc() error不是不支持它(如运行原因/编译代码的原因)类型检查其他?

有一些实际问题,但我相信对于func它们可能是可以解决的(通过发出 un/-wrapping 代码,类似于接口传递的工作方式)。 我写了一些关于Go 中的差异,并解释了我在底部看到的一些实际问题。 我并不完全相信它值得添加。 即,我不确定它是否能自行解决重要问题。

具有潜在的巨大缺点,它会破坏 funcs 与 funcs 的比较(因为包装的 func 将不等于它包装的 func)。

funcs 没有可比性。

无论如何,TBH,对于这个问题,所有这些似乎都有些偏离主题:)

仅供参考:我就是这样做的。 这不是很好,但它确实是类型安全的。 (同样的事情可以为 #19814 FWIW 做)

我参加聚会有点晚了,但我也想和大家分享一下我在围棋四年后的感受:

  • 多值回报是一个巨大的错误。
  • Nil-able 接口是一个错误。
  • 指针不是“可选”的同义词,应该使用可区分的联合来代替。
  • 如果 JSON 文档中未包含必填字段,则 JSON 解组器应该返回错误。

在过去的 4 年中,我发现了许多与之相关的问题:

  • 出错时返回垃圾数据。
  • 语法混乱(出现错误时返回零值)。
  • 多错误返回(混淆 API,请不要这样做!)。
  • 指向指向nil指针的非 nil 接口(让人们把“Go 是一种简单的语言”声明听起来像是一个坏笑话的人搞糊涂了)。
  • 未经检查的 JSON 字段使服务器崩溃(是的!)。
  • 未经检查的返回指针使服务器崩溃,但没有人记录返回的指针表示可选(可能是类型),因此可能是nil (是的!)

然而,修复所有这些问题所需的更改需要一个真正向后不兼容的 Go 2.0.0(不是 Go2)版本,我想这永远不会实现。 反正...

错误处理应该是这样的:

// Divide returns either a float64 or an arbitrary error
func Divide(dividend, divisor float64) float64 | error {
  if dividend == 0 {
    return errors.New("dividend is zero")
  }
  if divisor == 0 {
    return errors.New("divisor is zero")
  }
  return dividend / divisor
}

func main() {
  // type-switch statements enforce completeness:
  switch v := Divide(1, 0).(type) {
  case float64:
    log.Print("1/0 = ", v)
  case error:
    log.Print("1/0 = error: ", v)
  }

  // type-assertions, however, do not:
  divisionResult := Divide(3, 1)
  if v, ok := divisionResult.(float64); ok {
    log.Print("3/1 = ", v)
  }
  if v, ok := divisionResult.(error); ok {
    log.Print("3/1 = error: ", v.Error())
  }
  // yet they don't allow asserting types not included in the union:
  if v, ok := divisionResult.(string); ok { // compile-time error!
    log.Print("3/1 = string: ", v)
  }
}

接口不能替代受歧视的联合,它们是两种完全不同的动物。 编译器确保可区分联合上的类型切换是完整的,这意味着案例涵盖所有可能的类型,如果您不想要这样,那么您可以使用类型断言语句。

我经常看到人们对 _non-nil 接口到 nil 值_ 感到完全困惑https : error接口指向指向nil的自定义错误类型的指针时,人们会感到困惑,这就是我认为使接口为零是错误的原因之一。

界面就像接待员,当你和它说话时,你知道它是一个人,但在 Go 中,它可能是一个纸板人物,如果你试图和它说话,世界会突然崩溃。

区分联合应该用于可选(可能类型)并且将nil指针传递给接口应该会导致恐慌:

type CustomErr struct {}
func (err *CustomErr) Error() string { return "custom error" }

func CouldFail(foo int) error | nil {
  var err *customErr
  if foo > 10 {
    // you can't return a nil pointer as an interface value
    return err // this will panic!
  }
  // no error
  return nil
}

func main() {
  // assume no error
  if err, ok := CouldFail().(error); ok {
    log.Fatalf("it failed, Jim! %s", err)
  }
}

指针和可能类型不可互换。 对可选类型使用指针是不好的,因为它会导致 API 混乱:

// P returns a pointer to T, but it's not clear whether or not the pointer
// will always reference a T instance. It might be an optional T,
// but the documentation usually doesn't tell you.
func P() *T {}

// O returns either a pointer to T or nothing, this implies (but still doesn't guarantee)
// that the pointer is always expected to not be nil, in any other case nil is returned.
func O() *T | nil {}

然后还有 JSON。 联合永远不会发生这种情况,因为编译器强制您在使用之前检查它们。 如果 JSON 文档中不包含必填字段(包括指针类型的字段),则 JSON 解组器应该会失败:

type DataModel struct {
  // Optional needs to be type-checked before use
  // and is therefore allowed to no be included in the JSON document
  Optional string | nil `json:"optional,omitempty"`
  // Required won't ever be nil
  // If the JSON document doesn't include it then unmarshalling will return an error
  Required *T `json:"required"`
}

聚苯乙烯
我目前也在进行函数式语言设计,这就是我在那里使用可区分联合进行错误处理的方式:

read = (s String) -> (Array<Byte> or Error) => match s {
  "A" then Error<NotFound>
  "B" then Error<AccessDenied>
  "C" then Error<MemoryLimitExceeded>
  else Array<Byte>("this is fine")
}

main = () -> ?Error => {
  // assume the result is a byte array
  // otherwise throw the error up the stack wrapped in a "type-assertion-failure" error
  r = read("D") as Array<Byte>
  log::print("data: %s", r)
}

我很想看到这有一天成为现实。 所以让我们看看我是否能帮上忙:

也许问题在于我们试图在提案中涵盖太多内容。 我们可以使用一个简化版本,带来大部分价值,以便在短期内将其添加到语言中会容易得多。

在我看来,这个简化版本只与nil 。 以下是主要想法(几乎所有想法都已在评论中提及):

  1. 只允许| “歧视联合”的
    <any pointer type> | nil
    任何指针类型的位置:指针、函数、通道、切片和映射(Go 指针类型)
  2. 禁止将nil分配给裸指针类型。 如果要分配 nil,则类型需要为<pointer type> | nil 。 例如:
var n *int       = nil // Does not compile, wrong type
var n *int | nil = nil // Ok!

var set map[string] bool       = nil // Does not compile
var set map[string] bool | nil = nil // Ok!

var myFunc func(int) err       = nil // Nope!
var myFunc func(int) err | nil = nil // All right.

这些是主要的想法。 以下是从主要思想中衍生出来的思想:

  1. 您不能声明一个裸指针类型的变量并使其未初始化。 如果你想这样做,那么你需要添加| nil区分类型
var maybeAString *string       // Wrong: invalid initial value
var maybeAString *string | nil // Good
  1. 您可以将裸指针类型分配给“nilable”指针类型,但不能反过来:
var value int = 42
var barePointer *int = &value          // Valid
var nilablePointer *int | nil = &value // Valid

nilablePointer = barePointer // Valid
barePointer = nilablePointer // Invalid: Incompatible types
  1. 正如其他人指出的那样,从“nilable”指针类型中获取值的唯一方法是通过类型开关。 例如,按照上面的例子,如果我们真的想将nilablePointer的值赋给barePointer ,那么我们需要这样做:
switch val := nilablePointer.(type) {
  case *int:
    barePointer = val // Yeah! Types are compatible now. It is imposible that "val = nil"
  case nil:
    // Do what you need to do when nilablePointer is nil
}

就是这样。 我知道有区别的联合可以用于更多用途(特别是在返回错误的情况下),但我想说,坚持我上面写的内容,我们会以更少的努力为语言带来巨大的价值,而且没有使事情变得过于复杂。
我从这个简单的建议中看到的好处:

  • a)没有零指针错误。 好吧,从来没有 4 个词意味着那么多。 这就是为什么我觉得有必要从另一个角度说一下:没有 Go 程序会_EVER_再次出现nil pointer dereference错误! 💥
  • b) 您可以传递指向函数参数的指针,而无需交易“性能与意图”
    我的意思是,有时我想将结构传递给函数,而不是指向它的指针,因为我不想让该函数担心空值并强制它检查参数. 但是,我通常最终会传递一个指针以避免复制开销。
  • c)没有更多的 nil 地图! 是的! 我们将以关于“安全 nil-slices”和“不安全 nil-maps”的不一致结束(如果您尝试向它们写信,这将导致恐慌)。 地图将被初始化或类型map | nil ,在这种情况下,您需要使用类型开关😃

但这里还有一个无形的东西,它带来了很多价值:开发者安心。 您可以轻松地使用指针、函数、通道、映射等,而不必担心它们为零。 _我会为此付出代价的!_ 😂

从这个更简单的提案版本开始的一个好处是,它不会阻止我们在未来去寻找完整的提案,甚至一步一步地走(对我来说,这是允许有区别的错误返回的下一个自然步骤,但现在让我们忘记这一点)。

一个问题是,即使这个提案的简单版本也是向后不兼容的,但它可以通过gofix轻松修复:只需将所有指针类型声明替换为<pointer type> | nil

你怎么认为? 我希望这可以阐明并加速将 nil-safety 包含到语言中。 似乎这种方式(通过“有区别的联合”)是实现它的更简单、更正交的方式。

@alvaroloes

您不能声明一个裸指针类型的变量并使其未初始化。

这是问题的关键。 这不是 Go 做的事情——每种类型都有一个零值,句号。 否则你必须回答什么,例如make([]T, 100)呢? 您提到的其他事情(例如 nil 映射在写入时出现恐慌)是这条基本规则的结果。 (顺便说一句,我不认为说 nil-slice 比 map 更安全是真的 - 写入 nil-slice 会像写入 nil-map 一样恐慌)。

换句话说:你的提议实际上并没有那么简单,因为它与 Go 语言中一个非常基本的设计决策有很大的不同。

我认为 Go 所做的更重要的事情是让零值变得有用,而不是简单地将所有东西都赋予零值。 Nil map 是一个零值,但它没有用。 其实是有害的那么为什么在没有用的情况下不允许零值。 在这方面改变 Go 是有益的,但这个提议确实没有那么简单。

上面的提议看起来更像是 Swift 和其他人中的可选/非可选类型。 这很酷,但:

  1. 这会破坏几乎所有的程序,并且修复对 gofix 来说不是微不足道的。 您不能只用<pointer type> | nil替换所有内容,因为根据提案,这将需要类型切换来解压缩该值。
  2. 为了让它真正可用和可以忍受,Go 需要在这些选项周围有更多的语法糖。 以斯威夫特为例。 该语言中有许多特性专门用于处理可选项——保护、可选项绑定、可选项链、零合并等。我认为 Go 不会朝这个方向发展,但如果没有它们与可选项一起工作将是一件苦差事。

那么为什么在没有用的情况下不允许零值。

看上面。 这意味着一些看起来很便宜的东西有与之相关的非常重要的成本。

在这方面改变围棋将是有益的

它有好处,但这并不等同于有益。 它也有危害。 哪个更重取决于偏好和权衡。 Go 的设计师选择了这个。

FTR,这是该线程中的一般模式,也是对任何和类型概念的主要反驳之一 - 您需要说明零值是什么。 这就是为什么任何新想法都应该明确解决它的原因。 但有点令人沮丧的是,这些天在这里发帖的大多数人都没有阅读该主题的其余部分,并且倾向于忽略该部分。

🤔 啊哈! 我知道我显然遗漏了一些东西。 哦! “简单”一词具有复杂的含义。 好的,请随意从我之前的评论中删除“简单”一词。

对不起,如果这让你们中的一些人感到沮丧。 我的意图是尝试提供一些帮助。 我试图跟上线程,但我没有太多空闲时间花在这上面。

回到问题:所以似乎阻碍这一点的主要原因是零值。
在思考了一段时间并放弃了很多选择之后,我认为唯一可以增加价值并且值得一提的是以下内容:

如果我没记错的话,任何类型的零值都包括用 0 填充其内存空间。
正如您已经知道的,这对于非指针类型很好,但它是指针类型错误的来源:

type S struct {
    n int
}
var s S 
s.n  // Fine

var s *S
s.n // runtime error

var f func(int)
f() // runtime error

那么,如果我们:

  • 为每个指针类型定义一个有用的零值
  • 仅在第一次使用时对其进行初始化(延迟初始化)。

我认为这已在另一个问题中提出,不确定。 我写在这里是因为它解决了这个提案的主要阻碍点。

以下可能是指针类型的零值列表。 请注意,只有在访问值时才会使用这些零值。 我们可以称之为“动态零值”,它只是指针类型的一个属性:

| 指针类型 | 零值 | 动态零值 | 评论 |
| --- | --- | --- | --- |
| *T | nil | 新(T) |
| []T | nil | []T{} |
| 地图[T]U | nil | 地图[T]U{} |
| func | nil | noop | 因此,函数的动态零值什么都不做并返回零值。 如果返回值列表以error ,则返回一个默认错误,说明该函数是“无操作” |
| chan T | nil | make(chan T) |
| interface | nil | - | 默认实现,其中所有方法都使用上述noop函数进行初始化 |
| 歧视工会| nil | 第一类动态零值| |

现在,当这些类型被初始化时,它们将是nil ,就像现在一样。 不同之处在于访问nil的那一刻。 此时,将使用动态零值。 几个例子:

type S struct {
    n int
}
var s *S
if s == nil { // true. Nothing different happens here
...
}
s.n = 1       // At this moment the go runtime would check if it is nil, and if it is, 
              // do "s = new(S)". We could say the code would be replaced by:
/*
if s == nil {
    s = new(S)
}
s.n = 1
*/

// -------------
var pointers []*S = make([]*S, 100) // Everything as usual
for _,p := range pointers {
    p.n = 1 // This is translated to:
    /*
        if p == nil {
            p = new(S)
        }
        p.n = 1
    */
}

// ------------
type I interface {
    Add(string) (int, error)
}

var i I
n, err := i.Add("yup!") // This method returns 0, and the default error "Noop"
if err != nil { // This condition is true and the error is returned
    return err
}

我可能缺少实现细节和可能的困难,但我想首先关注这个想法。

主要的缺点是我们每次访问指针类型的值时都会添加一个额外的 nil-check。 但我会说:

  • 这是对我们获得的好处的一个很好的权衡。 同样的情况发生在数组/切片访问中的边界检查中,我们接受为它带来的安全性而支付性能损失。
  • 可以以与数组绑定检查相同的方式避免 nil 检查:如果指针类型已在当前范围内初始化,编译器可以知道这一点并避免添加 nil 检查。

有了这个,我们就有了前面评论中解释的所有好处,加上我们不需要使用类型开关来访问值(这仅适用于受歧视的联合),保持 go 代码像就是现在。

你怎么认为? 如果已经讨论过这一点,请道歉。 此外,我知道此评论提案与nil比歧视性工会更相关。 我可能会将其移至与 nil 相关的问题,但正如我所说,我将其发布在这里是因为它试图解决受歧视联合的主要问题:有用的零值。

回到问题:所以似乎阻碍这一点的主要原因是零值。

这是需要解决的一个重要的技术原因。 对我来说,最主要的原因是他们做出逐步修复断然不可能的(见上文)。 即对我个人而言,这不是如何实施它们的问题,而是我从根本上反对这个概念。
无论如何,哪个原因是“主要的”实际上是品味和偏好的问题。

那么,如果我们:

  • 为每个指针类型定义一个有用的零值
  • 仅在第一次使用时对其进行初始化(延迟初始化)。

如果您传递指针类型,这将失败。 例如

func F(p *T) {
    *p = 42 // same as if p == nil { p = new(T) } *p = 42
}

func G() {
    var p *T
    F(p)
    fmt.Println(p == nil) // Has to be true, as F can't modify p. But now F is silently misbehaving
}

这种讨论完全是新的。 引用类型的行为方式是有原因的,并不是 Go 开发人员没有考虑过:)

这是问题的关键。 这不是 Go 做的事情——每种类型都有一个零值,句号。 否则你必须回答什么,例如 make([]T, 100) 做什么?

如果T没有零值,则必须禁止此(和new(T) )。 您必须执行make([]T, 0, 100)然后使用append来填充切片。 重新切片更大( v[:0][:100] )也必须是一个错误。 [10]T基本上是一种不可能的类型(除非将切片断言到数组指针的能力添加到语言中)。 并且您需要一种方法将现有的 nilable 类型标记为非 nilable 以保持向后兼容性。

如果添加泛型,这将出现一个问题,因为您需要将所有类型参数视为不具有零值,除非它们满足某个界限。 类型的子集也基本上到处都需要初始化跟踪。 即使没有在它上面添加和类型,这本身就是一个相当大的变化。 这当然是可行的,但它确实对成本/收益分析的成本方面做出了重大贡献。 故意选择保持初始化简单(“总是有一个零值”)反而会产生使初始化更复杂的影响,而不是从第 1 天开始在语言中进行初始化跟踪。

这是需要解决的一个重要的技术原因。 对我来说,主要原因是他们绝对不可能逐步修复(见上文)。 即对我个人而言,这不是如何实施它们的问题,而是我从根本上反对这个概念。
无论如何,哪个原因是“主要的”实际上是品味和偏好的问题。

好的,我明白了。 我们只需要看看其他人的观点(我不是说你不这样做,我只是提出一个观点 :wink:),他们认为这是编写程序的强大工具。 它适合 Go 吗? 这取决于这个想法是如何执行和集成到语言中的,这就是我们在这个线程中试图做的(我猜)

如果您传递指针类型,这将失败。 例如(...)

我不太明白这个。 为什么这是一个失败? 您只是将一个值传递给函数参数,它恰好是一个具有nil值的指针。 然后您在函数内部修改该值。 预计您不会在函数之外看到这些效果。 让我评论一些例子:

// Augmenting your example with more comments:
func FCurrentGo(p *T) {
    // Here "p" is just a value, which happens to be a pointer type. Doing...
    *p = 42
    // ...without checking first for "nil" is the recipe for hiding a bug that will crash the entire program, 
    // which is exactly what is happening in current Go code bases

    // The correct code would be:
    if p == nil {
        // panic or return error
    }
    *p = 42
}

func FWithDynamicZero(p *T) {
    // Here, again, p is just a value of a pointer type. Doing...
    *p = 42
    // would allocate a new T and assign 42. It is true that this doesn't have any effect on the "outside
    // world", which could be considered "incorrect" because you expected the function to do that.
    // If you really want to be sure "p" is pointing to something valid in the "outside world", then
    // check that:
    if p == nil {
        // panic or return error
    }
    *p = 42
}

func main() {
    var p *T
    FCurrentGo(p) // This will crash the program
        FWithDynamicZero(p) // This won't have any effect on "p". This is expected because "p" is not pointing
                            // to anything. No crash here.
    fmt.Println(p == nil) // It is true, as expected
}

非指针接收器方法也发生了类似的情况,Go 的新手会感到困惑(但是一旦你理解了它,那就说得通了):

type Point struct {
    x, y int
}

func (p Point) SetXY(x, y int) {
    p.x = x
    p.y = y
}

func main() {
    p := Point{x: 1, y: 2}
    p.SetXY(24, 42)

    pointerToP := &Point{x: 1, y: 2}
    pointerToP.SetXY(24, 42)

    fmt.Println(p, pointerToP) // Will print "{1 2} &{1 2}", which could confuse at first
}

所以我们需要选择:

  • A) 崩溃失败
  • B) 当指针传递给函数时,指针指向的值的静默未修改导致失败。

这两种情况的修复方法是相同的:在做任何事情之前检查 nil。 但是,对我来说,A) 危害更大(整个应用程序崩溃!)。
B) 可以被视为“无声错误”,但我不会将其视为错误。 它仅在您将指针传递给函数时发生,正如我所展示的,有些情况下结构的行为方式相似。 这没有考虑它带来的巨大好处。

注意:我并不是要盲目地捍卫“我的”想法,我真的是在努力改进围棋(这已经非常好)。 如果还有一些其他点让这个想法不值得,那我也懒得扔掉,继续往其他方向思考

注 2:最终,这个想法仅适用于“nil”值,与受歧视的联合无关。 所以我会创建一个不同的问题来避免污染这个问题

好的,我明白了。 我们只需要看到其他人的观点(我不是说你不这样做,我只是提出一个观点)

不过,那把剑是双向的。 你说“阻止这件事的主要原因是”。该声明意味着我们都同意我们是否希望该提案的效果。我当然同意这是一个技术细节,阻碍了所提出的具体建议(或者至少,任何建议都应该说明该问题). 但我不喜欢将讨论悄悄地重新构建到一个平行世界,在那里我们假设每个人都真正想要它。

为什么这是一个失败?

因为接受指针的函数至少通常会承诺修改指针对象。 如果函数然后默默地什么都不做,我会认为这是一个错误。 或者至少,这是一个容易提出的论点,通过这种方式防止 nil-panic,你正在引入一类新的错误。

如果您将一个 nil 指针传递给一个期望在那里得到某些东西的函数,那就是一个错误——而且我看不到让这样一个有问题的软件默默地继续下去的实际价值。 通过支持非空指针,我可以看到在编译时捕获该错误的原始想法中的价值,但我不认为根本不允许该错误被捕获。

即可以这么说,您正在解决与非空指针的实际提议不同的问题:对于该提议,运行时恐慌不是问题,而只是一个症状- 问题是意外传递的错误nil用于不期望它的东西,并且此错误仅在运行时被捕获。

非指针接收器方法也发生了类似的情况

我不买这个比喻。 IMO 考虑是完全合理的

func Foo(p *int) { *p = 42 }

func main() {
    var v int
    Foo(&v)
    if v != 42 { panic("") }
}

是正确的代码。 我认为考虑不合理

func Foo(v int) { v = 42 }

func main( ){
    var v int
    Foo(v)
    if v != 42 { panic("") }
}

是正确的。 也许如果你是 Go 的绝对初学者,并且来自一种每个值都是一个引用的语言(尽管我真的很难找到一个 - 甚至 Python 和 Java 也只对大多数值进行引用)。 但是 IMO,针对这种情况进行优化是徒劳的,可以公平地假设人们对指针与值有一定的了解。 我认为即使是经验丰富的 Go 开发人员也会将指针接收器访问其字段的方法视为正确的方法,并且调用这些方法的代码是正确的。 事实上,这就是静态阻止nil指针的全部论据,很容易无意中让指针为零并且正确的代码在运行时失败。

这两种情况的修复方法是相同的:在做任何事情之前检查 nil。

IMO 当前语义中的修复是检查 nil 并在有人通过 nil 时将其视为错误。 就像,在你的例子中你写

// The correct code would be:
if p == nil {
    // panic or return error
}
*p = 42

但我不认为该代码是正确的。 nil -check 没有任何作用,因为取消引用nil已经引起恐慌。

但是,对我来说,A) 危害更大(整个应用程序崩溃!)。

这很好,但请记住,很多人会强烈反对这一点。 我个人认为崩溃总是比继续使用损坏的数据和错误的假设更可取。 在理想的世界中,我的软件没有错误,也永远不会崩溃。 在一个不太理想的世界中,我的程序将有错误,并在检测到它们时崩溃而安全失败。 在最糟糕的世界中,我的程序会有错误,并且在遇到错误时会继续造成严重破坏。

不过,那把剑是双向的。 你说“阻止这个的主要原因是”。 该声明意味着我们都同意是否希望该提案产生效果。 我当然同意这是一个技术细节,阻碍了所提出的具体建议(或者至少,任何建议都应该说明这个问题)。 但我不喜欢将讨论悄悄地重新定义为一个平行世界,我们假设每个人都真正想要它。

好吧,我不想暗示这一点。 如果是这样理解,那么我可能没有选择正确的词,我道歉。 我只是想为可能的解决方案提供一些想法,就是这样。

我写了 _"...似乎阻止这个的主要原因是...."_ 基于你的句子 _"这是问题的关键"_ 指的是零值。 这就是为什么我认为零值是阻碍这一点的主要因素。 所以这是我的错误假设。

关于静默处理nil与在编译时检查它们:我同意最好在编译时检查它们。 当我专注于解决所有类型都应该具有零值问题时,“动态零值”只是对原始建议的迭代。 一个额外的动机是我_认为_这也是歧视工会提案的主要阻碍。
如果我们只关注与 nil 相关的问题,我宁愿在编译时检查非 nil 指针类型。

我想说,在某些时候,我们(“我们”指的是整个 Go 社区)需要接受_某种_的变化。 例如:如果有一个很好的解决方案可以完全避免nil错误,而阻碍这一点的是设计决策“所有类型的值都为零并且它由 0 组成”,那么我们可以考虑这个想法如果该决定带来价值,则对其进行一些调整或更改。

我说这句话的主要原因是你的句子_“每种类型都有一个零值,句号”_。 我通常不喜欢“写句号”。 不要误会我的意思! 我完全接受你的想法,这只是我的想法:我不喜欢教条,因为它们可以隐藏可以导致更好解决方案的路径。

最后,关于这个:

这很好,但请记住,很多人会强烈反对这一点。 我个人认为崩溃总是比继续使用损坏的数据和错误的假设更可取。 在理想的世界中,我的软件没有错误,也永远不会崩溃。 在一个不太理想的世界中,我的程序将有错误,并在检测到它们时崩溃而安全失败。 在最糟糕的世界中,我的程序会有错误,并且在遇到错误时会继续造成严重破坏。

我完全同意这种说法。 大声失败总是比默默失败好。 但是,Go 中有一个问题:

  • 如果你有一个包含数千个 goroutine 的应用程序,其中一个未处理的恐慌会导致整个程序崩溃。 这与其他语言不同,在其他语言中,只有恐慌的线程崩溃

把它放在一边(虽然它很危险),那么这个想法是为了避免一整类失败( nil相关的失败)。

因此,让我们继续对此进行迭代并尝试找到解决方案。

感谢您的时间和精力!

我希望看到 rust 的可区分联合语法而不是 haskell 的 sum 类型,它允许命名变体并允许更好的模式匹配语法提议。
实现可以像带有标记字段(uint 类型,取决于变体的数量)和联合字段(保存数据)的结构一样完成。
封闭的变体集需要此功能(状态表示会更容易和更清晰,并进行编译时检查)。 根据接口及其表示的问题,我认为它们在 sum 类型中的实现一定不会只是 sum 类型的另一种情况,因为接口是关于满足某些要求的任何类型,但 sum 类型用例是不同的。

句法:

type Type enum {
         Tuple (int,int),
         One int,
         None,
};

在上面的例子中,大小是 sizeof((int,int))。
模式匹配可以使用新创建的匹配运算符完成,也可以在现有的 switch 运算符中完成,就像:

var a Type
switch (a) {
         case Tuple{(b,c)}:
                    //do something
         case One{b}:
                    //do something else
         case None:
                    //...
}

创建语法:
var a Type = Type{One=12}
请注意,在枚举实例构造中只能指定一种变体。

零值(问题):
我们可以按字母顺序对名称进行排序,枚举的零值将是已排序成员列表中第一个成员类型的零值。

PS 零值问题的解决方案大多由协议定义。

我认为将总和的零值保留为第一个用户定义的总和字段的零值会不那么令人困惑,也许

我认为将总和的零值保留为第一个用户定义的总和字段的零值会不那么令人困惑,也许

但是使零值取决于字段声明顺序,我认为它更糟。

有人写过设计文档吗?

我有一个:
19412-discriminated_unions_and_pattern_matching.md.zip

我改变了这个:

我认为将总和的零值保留为第一个用户定义的总和字段的零值会不那么令人困惑,也许

现在在我的提案中,关于零值(问题)的协议移至 urandoms 位置。

UPD:设计文档已更改,小修复。

我最近有两个用例,我需要内置 sum 类型:

  1. 正如预期的那样,AST 树表示。 最初找到了一个乍一看是一个解决方案的库,但他们的方法是拥有一个包含大量空字段的大型结构。 最糟糕的两个世界 IMO。 当然没有类型安全。 而是写了我们自己的。
  2. 有一个预定义的后台任务队列:我们有一个正在开发中的搜索服务,我们的搜索操作可能会过长等等。所以我们决定通过将搜索索引操作任务发送到通道来在后台执行它们。 然后调度员将决定如何进一步处理它们。 可以使用访问者模式,但对于简单的 gRPC 请求来说,这显然是一种矫枉过正。 至少说得不是特别清楚,因为它引入了调度员和访客之间的联系。

在这两种情况下都实现了这样的东西(在第二个任务的例子中):

type Task interface {
    task()
}

type SearchAdd struct {
    Ctx   context.Context
    ID    string
    Attrs Attributes
}

func (SearchAdd) task() {}

type SearchUpdate struct {
    Ctx         context.Context
    ID          string
    UpdateAttrs UpdateAttributes
}

func (SearchUpdate) task() {}

type SearchDelete struct {
    Ctx context.Context
    ID  string
}

func (SearchDelete) task() {}

进而

task := <- taskChannel

switch v := task.(type) {
case tasks.SearchAdd:
    resp, err := search.Add(task.Ctx, &search2.RequestAdd{…}
    if err != nil {
        log.Error().Err(err).Msg("blah-blah-blah")
    } else {
        if resp.GetCode() != search2.StatusCodeSuccess  {
            …
        } 
    }
case tasks.SearchUpdate:
    …
case tasks.SearchDelete:
    …
}

这几乎是好的。 Go 没有提供完整的类型安全,即添加新的搜索索引操作任务后不会出现编译错误。

恕我直言,使用 sum 类型是此类任务的最清晰解决方案,这些任务通常由访问者和一组调度员解决,其中访问者的功能并不多见且访问者本身是固定类型。

我真的相信有类似的东西

type Task oneof {
    // SearchAdd holds a data for a new record in the search index
    SearchAdd {
        Ctx   context.Context
        ID    string
        Attrs Attributes   
    }

    // SearchUpdate update a record
    SearchUpdate struct {
        Ctx         context.Context
        ID          string
        UpdateAttrs UpdateAttributes
    }

    // SearchDelete delete a record
    SearchDelete struct {
        Ctx context.Context
        ID  string
    }
}

+

switch task {
case tasks.SearchAdd:
    // task is tasks.SearchAdd in this scope
case tasks.SearchUpdate:
case tasks.SearchDelete:
}

与 Go 在当前状态下允许的任何其他方法相比,精神上会更加 Goish。 不需要 Haskellish 模式匹配,只要跳到某种类型就足够了。

哦,错过了语法建议的重点。 修理它。

两个版本,一个用于泛型 sum 类型和 sum 类型用于枚举:

通用和类型

type Sum oneof {
    T₁ TypeDecl₁
    T₂ TypeDecl₂
    …
    Tₙ TypeDeclₙ
}

其中T₁ ... Tₙ是与Sum处于同一级别的类型定义( oneof将它们暴露在其范围之外)并且Sum声明一些只有T₁ ... Tₙ满足的接口。

处理类似于我们的(type)开关,除了它是在oneof对象上隐式完成的,并且必须有一个编译器检查是否列出了所有变体。

真实类型安全枚举

type Enum oneof {
    Value = iota
}

与 const 的iota非常相似,除了只有明确列出的值是枚举,其他的都不是。

switch task {
case tasks.SearchAdd:
    // task is tasks.SearchAdd in this scope
case tasks.SearchUpdate:
case tasks.SearchDelete:
}

与 Go 在当前状态下允许的任何其他方法相比,精神上会更加 Goish。 不需要 Haskellish 模式匹配,只要跳到某种类型就足够了。

我不认为操纵task变量的含义是个好主意,尽管可以接受。

```go
switch task {
case tasks.SearchAdd:
    // task is tasks.SearchAdd in this scope
case tasks.SearchUpdate:
case tasks.SearchDelete:
}

与 Go 在当前状态下允许的任何其他方法相比,精神上会更加 Goish。 不需要 Haskellish 模式匹配,只要跳到某种类型就足够了。

我不认为操纵任务变量的含义是个好主意,尽管可以接受。
``

祝你的访客好运。

@sirkon你对访客是什么意思? 顺便说一句,我喜欢这种语法,但是开关应该这样写:

switch task {
case Task.SearchAdd:
    // task is Task.SearchAdd in this scope
case Task.SearchUpdate:
case Task.SearchDelete:
}

还有Task是多少? 例如:

var task Task

会是nil吗? 如果是这样, switch是否应该有一个额外的case nil
或者它会被初始化为第一种类型? 这会很尴尬,因为然后类型声明的顺序以一种以前没有的方式重要,但是对于数字枚举可能没问题。

我假设这等价于switch task.(type)但切换需要所有情况都在那里,对吗? 比如……如果你错过了一种情况,编译错误。 并且不允许default 。 那正确吗?

你说的访客是什么意思?

我的意思是它们是 Go 中唯一类型安全的选项,用于此类功能。 对于某些情况(有限数量的预定义替代方案),情况要糟糕得多。

任务的无价值是什么? 例如:

var task Task

恐怕它应该是 Go 中的 nilable 类型,因为这样

或者它会被初始化为第一种类型?

太奇怪了,尤其是对于预期的目的。

我假设这相当于 switch task.(type) 但 switch 需要所有情况都在那里,对吗? 比如……如果你错过了一种情况,编译错误。

是的,没错。

并且不允许默认。 那正确吗?

不,允许默认值。 虽然气馁。

PS 我似乎对 Go @ianlancetaylor和其他 Go 人有关于 sum 类型的想法。 看起来 nilness 使它们非常容易出现 NPD,因为 Go 无法控制 nil 值。

如果它是零,那么我想这很好。 我希望case nil是 switch 语句的要求。 之前做一个if task != nil也可以,我只是不太喜欢它:|

这也会被允许吗?

type Foo oneof {
  A = 3
  B = "3"
  C = 3.0
  D = struct { E bool }{ true }
}

这也会被允许吗?

type Foo oneof {
  A = 3
  B = "3"
  C = 3.0
  D = struct { E bool }{ true }
}

那么,没有常量,只有

type Foo oneof {
    A <type reference>
}

或者

type Foo oneof {
    A = iota
    B
    C
}

或者

type Foo oneof {
    A = 1
    B = 2
    C = 3
}

没有iotas和values的组合。 或与控制值相结合,不应重复。

FWIW,我发现最新的泛型设计有趣的一件事是它展示了另一个场所,可以解决至少一些 sum 类型的用例,同时避免有关零值的陷阱。 它定义了析取契约,它们在某种程度上是和式的,但是因为它们描述的是约束而不是类型,所以不需要具有零值(因为您不能声明该类型的变量)。 也就是说,至少可以编写一个函数,该函数采用一组有限的可能类型,并对该组进行编译时类型检查。

现在,当然,原样的设计并不真正适用于此处预期的用例:析取仅列出底层类型或方法,因此仍然广泛开放。 当然,即使作为一般想法,它也非常有限,因为您无法实例化泛型(或 sum-ish-taking)函数或值。 但是 IMO 表明,解决某些总和用例的设计空间比总和类型本身的想法要大得多。 因此,对总和的思考更加关注特定的解决方案,而不是特定的问题。

反正。 只是觉得很有趣。

@Merovius对最新的通用设计能够处理和类型的一些用例提出了一个很好的观点。 例如,这个在线程早期使用的函数:

func addOne(x int|float64) int|float64 {
    switch x := x.(type) {
    case int:
        return x + 1
    case float64:
         return x + 1
    }
}

会成为:

contract intOrFloat64(T) {
    T int, float64
}

func addOne(type T intOrFloat64) (x T) T {
    return x + 1
}

就 sum 类型本身而言,如果泛型最终出现,我会比现在更加怀疑引入它们的好处是否会超过像 Go 这样的简单语言的成本。

但是,如果要采取某些措施,那么 IMO 最简单且破坏性最小的解决方案将是@ianlancetaylor的“受限接口”概念,该概念的实现方式与当今的“不受限制”接口完全相同,但只能满足按指定的类型。 事实上,如果你从通用设计的书中翻出一页,并将类型约束作为接口块的第一行:

type intOrFloat64 interface{ type int, float64 }    

那么这将完全向后兼容,因为您根本不需要新关键字(例如restrict )。 您仍然可以向接口添加方法,如果所有指定类型都不支持这些方法,则会出现编译时错误。

我认为将值分配给受限接口类型的变量完全没有问题。 如果 RHS 上的值的类型(或无类型文字的默认类型)与指定类型之一不完全匹配,那么它根本不会编译。 所以我们有:

var v1 intOrFloat64 = 1        // compiles, dynamic type int
var v2 intOrFloat64 = 1.0      // compiles, dynamic type float64
var v3 intOrFloat64 = 1 + 2i   // doesn't compile, complex128 is not a specified type

对于类型切换与指定类型不匹配的情况,这将是一个编译时错误,并且可以实施穷举检查。 但是,仍然需要类型断言来将受限接口值转换为其动态类型的值,就像今天一样。

零值对于这种方法来说不是问题(或者无论如何也不会像现在的接口那样有更多的问题)。 受限接口的零值将是nil (暗示它当前不包含任何内容)并且指定的类型当然会有自己的零值,在内部,这将是nil对于可空类型。

所有这些对我来说似乎完全可行,但正如我之前所说,获得的编译时安全性确实值得额外的复杂性 - 我有我的怀疑,因为我从来没有真正感觉到在我自己的编程中需要 sum 类型。

IIUC泛型的东西不会是动态类型,所以这整点都不成立。 然而,如果允许接口作为契约工作(我怀疑),它不会解决详尽的检查和枚举,这就是(我认为,也许不是?) sumtypes 是关于的。

@alanfo@Merovius感谢您的提示; 有趣的是,这次讨论正朝着这个方向发展:

我想在一瞬间改变观点:我试图理解为什么不能用允许上述类型限制的参数化接口完全替换合同。 目前我没有看到任何强有力的技术原因,除了这样的“sum”接口类型,当用作“sum”类型时,会希望将可能的动态值限制为接口中枚举的类型,而 - 如果在合约位置使用相同的接口 - 接口中的枚举类型需要作为基础类型才能成为合理有用的通用限制。

@Goodwine
我并不是建议泛型设计可以解决人们可能想用和类型做的所有事情 - 正如@Merovius在他的上

但是,泛型设计将使人们能够编写一个函数,该函数对编译器将强制执行的一组有限类型进行操作,而这是我们目前根本无法做到的。

就受限接口而言,编译器将知道可以使用的精确类型,因此在类型 switch 语句中进行详尽检查变得可行。

@格里瑟默

我对你所说的话感到困惑,因为我认为泛型设计文档草案解释得很清楚(在“为什么不使用接口而不是契约”部分)为什么后者被认为是比前者更好的表达泛型约束的工具。

特别是,契约可以表达类型参数之间的关系,因此只需要一个契约。 它的任何类型参数都可以用作合约中列出的方法的接收器类型。

对于接口,无论是否参数化,都不能这样说。 如果它们有任何约束,每个类型参数都需要一个单独的接口。

这使得使用接口表达类型参数之间的关系变得更加尴尬,尽管如图示例所示并非不可能。

但是,如果您认为我们可以通过向接口添加类型约束,然后将它们用于泛型和求和类型的目的,“一石二鸟”,那么(除了您提到的问题)我认为您是这可能是正确的,这在技术上是可行的。

我想就泛型而言,接口类型约束是否可以包括“非内置”类型并不重要,尽管需要找到某种方式将它们限制为确切类型(而不是派生类型)所以它们适合和类型。 如果我们要坚持使用当前的关键字,也许我们可以将const type用于后者(甚至只是const )。

@griesemer参数化接口类型不能直接替代合同的原因有几个。

  1. 类型参数与其他参数化类型相同。
    在像

    type C2(type T C1) interface { ... }
    

    类型参数T存在于接口本身之外。 任何作为T传递的类型参数都必须已知以满足契约C1 ,并且接口的主体不能进一步约束T 。 这与合约参数不同,合约参数由于传入合约主体而受到合约主体的约束。 这意味着函数的每个类型参数在作为参数传递给任何其他类型参数的约束之前都必须独立约束。

  2. 无法在接口主体中命名接收器类型。
    接口必须让您编写如下内容:

    type C3(type U C1) interface(T) {
        Add(T) T
    }
    

    其中T表示接收器类型。

  3. 一些接口类型不会满足自己作为通用约束。
    任何依赖于接收器类型的多个值的操作都与动态分派不兼容。 因此,这些操作不能用于接口值。 这意味着接口不会满足自身(例如,作为受同一接口约束的类型参数的类型参数)。 这将是令人惊讶的。 一种解决方案是根本不允许为此类接口创建接口值,但这将不允许在此处设想用例。

至于区分底层类型约束和类型标识约束,有一种方法可能有效。 想象一下我们可以定义自定义约束,比如

contract (T) indenticalTo(U) {
    *T *U
}

(在这里,我使用了一种发明的符号来指定一个单一类型作为“接收者”。我将一个带有显式接收器类型的契约发音为“约束”,就像带有接收器的 func 发音为“方法”一样。合约名称后面的参数是普通类型参数,不能出现在约束主体中约束子句的左侧。)

因为文字指针类型的基础类型是它自己,所以这个约束意味着TU 。 因为这被声明为约束,所以您可以将(identicalTo(int)), (identicalTo(uint)), ...为约束析取。

虽然合同可能有助于表达某种类型的总和类型,但我认为您不能用它们表达通用的总和类型。 从我从草案中看到的情况来看,必须列出具体类型,所以你不能写这样的东西:

contract Foo(T, U) {
    T U, int64
}

哪一个需要表达未知类型和一个/多个已知类型的通用和类型。 即使设计确实允许这样的构造,它们在使用时也会看起来很奇怪,因为这两个参数实际上是同一个东西。

我一直在思考,如果接口被扩展为包含类型约束,然后用于替换设计中的契约,那么泛型设计草案可能会如何改变。

如果我们考虑不同数量的类型参数,可能最容易分析这种情况:

无参数

没变 :)

一参数

这里没有真正的问题。 仅当需要引用自身的类型参数和/或其他一些独立的固定类型来实例化接口时,才需要参数化接口(与非通用接口相反)。

两个或多个参数

如前所述,如果每个类型参数都需要约束,则需要单独对其进行约束。

只有在以下情况下才需要参数化接口:

  1. 类型参数引用自身。

  2. 接口引用了在类型参数部分_已经声明_的另一个类型参数(大概我们不想在这里回溯)。

  3. 需要一些其他独立的固定类型来实例化接口。

其中(2)确实是唯一麻烦的情况,因为它会排除相互引用的类型参数,例如在图形示例中。 无论是先声明 'Node' 还是 'Edge',其约束接口仍然需要另一个作为类型参数传递。

但是,如设计文档中所示,您可以通过在顶级声明非参数化(因为它们不指代自己)NodeInterface 和 EdgeInterface 来解决此问题,因为无论它们是什么,相互引用都没有问题声明顺序。 然后,您可以使用这些接口来约束 Graph 结构的类型参数及其关联的“New”方法的类型参数。

因此,即使合同的想法更好,这里看起来也不存在任何无法解决的问题。

据推测, comparable现在可以成为内置接口而不是合约。

当然,接口可以彼此嵌入,因为它们已经可以了。

我不确定如何处理指针方法问题(在那些需要在合同中指定这些问题的情况下),因为您无法为接口方法指定接收器。 也许需要一些特殊的语法(例如在方法名称前加星号)来指示指针方法。

现在转向@stevenblenkinsop的观察,我想知道如果参数化接口根本不允许以任何方式限制它们自己的类型参数,是否会让生活更轻松? 除非有人能想到一个合理的用例,否则我不确定这是否真的是一个有用的功能。

就我个人而言,我并不认为某些接口类型将无法满足作为通用约束的要求。 接口类型在任何情况下都不是有效的接收器类型,因此可以没有方法。

尽管 Steven 的内置函数 sameTo() 的想法可行,但在我看来,指定和类型可能很冗长。 我更喜欢一种允许将整行类型指定为精确的语法。

@urandom是正确的,当然,作为泛型草案目前的立场,只能列出具体(内置或聚合内置)类型。 但是,如果对泛型和和类型都使用受限制的接口,这显然必须改变。 所以我不排除在统一环境中允许这样的事情:

interface Foo(T) {
    const type T, int64  // 'const' indicates types are exact i.e. no derived types
}

为什么我们不能只是在语言中添加歧视性工会,而不是发明另一种方式来代替他们的缺席?

@griesemer你可能知道也可能不知道,但我从一开始就赞成使用接口来指定约束:) 我不再认为那篇文章中提出的确切想法是要走的路(尤其是那些事情我建议解决运营商)。 而且我确实比前一个更喜欢合约设计的最新迭代。 但总的来说,我完全同意(可能是扩展的)接口作为约束是可行的并且值得考虑。

@urandom

我不认为你可以用它们表达通用和类型

我想重申一下,我的观点不是“你可以用它们构建和类型”,而是“你可以解决和类型用它们解决的一些问题”。 如果您的问题陈述是“我想要求和类型”,那么求和类型是唯一的解决方案也就不足为奇了。 我只是想表达的是,如果我们专注于您想与他们一起解决的问题,那么没有他们也有可能。

@alanfo

这使得使用接口表达类型参数之间的关系变得更加尴尬,尽管如图示例所示并非不可能。

我认为“尴尬”是主观的。 就我个人而言,我发现使用参数化接口更自然,图形示例是一个很好的说明。 对我来说,Graph 是一个实体,而不是一种 Edge 和一种 Node 之间的关系。

但是 TBH,我不认为它们中的任何一个真的或多或少尴尬 - 你编写几乎完全相同的代码来表达几乎完全相同的东西。 FWIW,对此有现有技术。 Haskell 类型类的行为很像接口,正如那篇 wiki 文章指出的那样,使用多参数类型类来表达类型之间的关系是一件非常正常的事情。

@stevenblenkinsop

无法在接口主体中命名接收器类型。

您解决这个问题的方式是在使用站点使用类型参数。 IE

type Adder(type T) interface {
    Add(t T) T
}

func Sum(type T Adder(T)) (vs []T) T {
    var t T
    for _, v := range vs {
        t = t.Add(v)
    }
    return t
}

这需要注意统一的工作方式,以便您可以允许自引用类型参数,但我认为它可以起作用。

你的 1. 和 3. 我真的不明白,我不得不承认。 我会从一些具体的例子中受益。


无论如何,在继续讨论结束时放弃这个有点不诚实,但这可能不是讨论泛型设计细节的正确问题。 我提出它只是为了拓宽这个问题的设计空间:) 因为感觉自从新的想法被引入关于 sum 类型的讨论以来已经有一段时间了。

```go
switch task {
case tasks.SearchAdd:
    // task is tasks.SearchAdd in this scope
case tasks.SearchUpdate:
case tasks.SearchDelete:
}

与 Go 在当前状态下允许的任何其他方法相比,精神上会更加 Goish。 不需要 Haskellish 模式匹配,只要跳到某种类型就足够了。
我不认为操纵任务变量的含义是个好主意,尽管可以接受。

祝你的访客好运。

为什么你认为模式匹配不能在 Go 中完成? 如果您缺少模式匹配的示例,请参见 Rust。

@Merovius re:“对我来说,图是一个实体”

它是编译时实体还是在运行时有表示? 契约和接口之间的主要区别之一是接口是一个运行时对象。 它参与垃圾收集,拥有指向其他运行时对象的指针,等等。 从合约转换为接口意味着引入一个新的临时运行时对象,该对象具有指向它包含的节点/顶点的指针(有多少?)根据函数的需要,以自己的方式获取指向图形各个部分的参数。

你的直觉可能会因为合约使用“Graph”而被误导,因为“Graph”看起来像对象,合约并没有真正指定任何特定的子图; 它更像是定义一组稍后使用的术语,就像您在数学或法律中所做的那样。 在某些情况下,您可能需要图形合约和图形接口,从而导致恼人的名称冲突。 不过,我想不出更好的名字。

相比之下,有区别的联合是一个运行时对象。 虽然不限制实现,但您需要考虑它们的数组可能是什么样的。 一个 N 项数组需要 N 个鉴别器和 N 个值,并且有多种方法可以完成。 (Julia 有有趣的表示,有时将鉴别器和值放在单独的数组中。)

为了建议减少interface{}方案目前到处发生的错误,但要删除|运算符的连续键入,我建议如下:

type foobar union {
    int
    float64
}

仅用这种类型安全替换许多interface{}用例将是库的巨大收益。 只需查看加密库中的一半内容就可以使用它。

诸如以下问题:啊,您输入的是ecdsa.PrivateKey而不是*ecdsa.PrivateKey - 这是一个仅支持 ecdsa.PrivateKey 的通用错误。 这些应该是明确的联合类型的简单事实会大大增加类型安全性。

虽然与int|float64相比,此建议占用更多空间,但它确实迫使用户考虑这一点。 保持代码库更干净。

为了建议减少interface{}方案目前到处发生的错误,但要删除|运算符的连续键入,我建议如下:

type foobar union {
    int
    float64
}

仅用这种类型安全替换许多interface{}用例将是库的巨大收益。 只需查看加密库中的一半内容就可以使用它。

诸如以下问题:啊,您输入的是ecdsa.PrivateKey而不是*ecdsa.PrivateKey - 这是一个仅支持 ecdsa.PrivateKey 的通用错误。 这些应该是明确的联合类型的简单事实会大大增加类型安全性。

虽然与int|float64相比,此建议占用更多空间,但它确实迫使用户考虑这一点。 保持代码库更干净。

看到这个(评论) ,这是我的建议。

实际上,我们可以将我们的想法引入语言。 这将导致存在两种执行 ADT 的本机方式,但使用不同的语法。

我对特性的建议,尤其是模式匹配,你的兼容性和从旧代码库的特性中受益的能力。

但看起来有点矫枉过正,不是吗?

此外,可以将 sum 类型设为nil作为默认值。 当然,每个开关都需要nil大小写。
模式匹配可以这样完成:
- 宣言

type U enum{
    A(int64),
    B(string),
}

-- 匹配

...
var a U
...
switch a {
    case A{b}:
         //process b here
    case B{b}:
         //...
    case nil:
         //...
}
...

如果不喜欢模式匹配 - 请参阅上面 Sirkon 的提议。

此外,可以将 sum 类型设为nil作为默认值。 当然,每个开关都需要nil大小写。

在编译时禁止非初始化值不是更容易吗? 对于需要初始化值的情况,我们可以将其添加到 sum 类型中:即

type U enum {
  None
  A(string)
  B(uint64)
}
...
var a U.None
...
switch a {
  case U.None: ...
  case U.A(str): ...
  case U.B(i): ...
}

此外,可以将 sum 类型设为nil作为默认值。 当然,每个开关都需要nil大小写。

在编译时禁止非初始化值不是更容易吗? 对于需要初始化值的情况,我们可以将其添加到 sum 类型中:即

破坏现有代码。

此外,可以将 sum 类型设为nil作为默认值。 当然,每个开关都需要nil大小写。

在编译时禁止非初始化值不是更容易吗? 对于需要初始化值的情况,我们可以将其添加到 sum 类型中:即

破坏现有代码。

没有任何带有 sum 类型的现有代码。 虽然我认为默认值应该是类型本身定义的东西。 要么是第一个条目,要么是按字母顺序排列的第一个条目,或者其他什么。

没有任何带有 sum 类型的现有代码。 虽然我认为默认值应该是类型本身定义的东西。 要么是第一个条目,要么是按字母顺序排列的第一个条目,或者其他什么。

我一开始就同意你的看法,但经过一些思考,联合的新保留名称之前可能已经在某些代码库(联合、枚举等)中使用过。

我认为检查 nil 的义务使用起来会很痛苦。

看起来像向后兼容性的重大变化,只能由 Go2.0 解决

没有任何带有 sum 类型的现有代码。 虽然我认为默认值应该是类型本身定义的东西。 要么是第一个条目,要么是按字母顺序排列的第一个条目,或者其他什么。

但是有很多现有的 go 代码都没有任何东西。 这肯定会带来突破性的变化。 更糟糕的是,gofix 和类似的工具只能将变量类型更改为选项(相同类型),至少会产生丑陋的代码,所有其他情况下它只会破坏世界上的一切。

如果不出意外一些东西。 但所有这些都是可以解决的技术障碍——例如,如果 sum-type 的零值是明确定义的,那么这个障碍就非常明显,如果没有,可能会“恐慌”。 更大的问题仍然是为什么某个选择是正确的,以及任何选择是否以及如何适合整个语言。 IMO,解决这些问题的最佳方法仍然是讨论总和类型解决特定问题或缺乏创建问题的具体案例。 经验报告

请特别注意,上面已经多次提到“不应该有零值并且应该禁止创建未初始化的值”和“默认值应该是第一个条目”。 因此,无论您认为它应该这样还是那样,都不会真正添加新信息。 但它使一个已经很庞大的线程变得更长,未来在其中找到相关信息变得更加困难。

让我们考虑reflect.Kind。 有一个 Invalid Kind,它的默认 int 值为 0。如果您有一个接受reflect.Kind 的函数,并且您传递了该类型的未初始化变量,则它最终将成为 Invalid。 如果可以假设,reflect.Kind 可以更改为 sum 类型,它或许应该保留将命名无效条目作为默认条目的行为,而不是依赖于 nil 值。

现在,让我们考虑 html/template.contentType。 Plain 类型是它的默认值,确实被 stringify 函数如此对待,因为它是后备。 在假设的总和未来中,您不仅仍然需要这种行为,而且为它使用 nil 值也是不可行的,因为 nil 对这种类型的用户没有任何意义。 在这里总是返回一个命名值几乎是强制性的,并且您对该值应该是什么有一个明确的默认值。

又是我的另一个例子,其中代数/可变参数/求和/任何数据类型都能很好地工作。

因此,我们使用没有事务的 noSQL 数据库(分布式系统,事务对我们不起作用),但出于显而易见的原因,我们喜欢数据完整性和一致性,并且必须解决并发访问问题,通常是通过单个复杂的条件更新查询记录(单条记录写入是原子的)。

我有一个新任务来编写一组可以插入、附加或删除的实体(只有这些操作中的一个)。

如果我们可以有类似的东西

type EntityOp oneof {
    Insert   Reference
    NewState string
    Delete   struct{}
}

该方法可能只是

type DB interface {
    …
    Capture(ctx context.Context, processID string, ops map[string]EntityOp) (bool, error)
}

求和时间的一个奇妙用途是在 AST 中表示节点。 另一种方法是用在编译时检查的option替换nil

@DemiMarie但是在今天的 Go 中,这个总和也可以是 nil,正如我上面提出的,我们可以简单地使 nil 成为每个枚举的变体,每个 switch 中都会有 case nil 但这个义务并没有那么糟糕,特别是如果我们想要这个功能而不破坏所有现有的 go 代码(目前我们拥有所有内容)

不知道它是否属于这里,但所有这些都留给我打字稿,其中存在称为“字符串文字类型”的非常酷的功能,我们可以这样做:

var name: "Peter" | "Consuela"; // string type with compile-time constraint

它就像字符串枚举,在我看来它比传统的数字枚举要好得多。

@Merovius
一个具体的例子是使用任意 JSON。
在 Rust 中,它可以表示为
枚举值{
空值,
布尔(布尔),
号码(号码),
字符串(字符串),
数组(向量),
对象(地图),
}

联合类型有两个优点:

  1. 自我记录代码
  2. 允许编译器或go vet检查联合类型的错误使用
    (例如,未检查所有类型的开关)

对于语法,以下应该与Go1兼容,就像类型别名一样

type Token = int | float64 | string

联合类型可以在内部实现为接口; 重要的是,使用联合类型可以使代码更具可读性并捕获错误,例如

var tok Token

switch t := tok.(type) {
case int:
    // do something
}

编译器应该引发错误,因为在 switch 中使用了非所有Token类型。

这样做的问题是(据我所知)没有办法将指针类型(或包含指针的类型,例如string )和非指针类型存储在一起。 即使是具有不同布局的类型也不起作用。 随时纠正我,但问题是精确的 GC 不能很好地处理变量,这些变量可以同时是指针和简单变量。

我们可以走隐式拳击的道路 - 就像interface{}目前所做的那样。 但我不认为这提供了足够的好处——它仍然看起来像一种美化的接口类型。 也许可以开发某种vet支票?

垃圾收集器需要从联合中读取标记位来确定布局。 这并非不可能,但会是运行时的重大变化,可能会减慢 gc。

也许可以开发某种兽医检查?

https://github.com/BurntSushi/go-sumtype

垃圾收集器需要从联合中读取标记位来确定布局。

这与接口存在的完全相同,当它们可以包含非指针时。 那个设计被明确地移开了。

go-sumtype很有趣,谢谢。 但是如果同一个包定义了两种联合类型会发生什么?

编译器可以在内部将联合类型实现为接口,但添加了统一的语法和标准类型检查。

如果有 N 个项目使用联合类型,每个项目都不同并且 N 足够大,那么引入一种方法可能是最好的解决方案。

但是如果同一个包定义了两种联合类型会发生什么?

没什么? 逻辑是每个类型的,并使用虚拟方法来识别实现者。 只需为虚拟方法使用不同的名称。

@skybrian IIRC 当前位图指定类型布局当前存储在一个地方。 为每个对象添加这样的东西会增加很多跳转,并使每个可选对象成为 GC 根。

这样做的问题是(据我所知)没有办法将指针类型(或包含指针的类型,例如字符串)和非指针类型存储在一起

我不相信这是必要的。 当指针映射匹配时,编译器可能会重叠类型的布局,否则不会。 当它们不匹配时,可以自由地将它们连续布置或使用当前用于接口的指针方法。 它甚至可以为结构成员使用非连续布局。

但我不认为这提供了足够的好处——它仍然看起来像一种美化的接口类型。

我的提议中,联合类型是 _exactly_ 一种美化的接口类型——联合类型只是接口的一个子集,它只允许存储一组枚举类型。 这可能使编译器可以自由地为某些类型集选择更有效的存储方法,但这是一个实现细节,而不是主要动机。

@rogpeppe - 出于好奇,我可以直接使用 sum 类型还是我明确需要将其转换为已知类型才能对它执行任何操作? 因为如果我必须不断地将它转换为已知类型,我真的不知道这比接口已经给我们带来了什么好处。 我看到的主要好处是编译时错误检查,因为解组仍然会在运行时发生,这更有可能是您看到传递无效类型的问题。 另一个好处是界面更受限制,我认为这不需要更改语言。

我可不可以做

type FooType int | float64

func AddOne(foo FooType) FooType {
    return foo + 1
}

// if this can be done, what happens here?
type FooType nil | int
func AddOne(foo FooType) FooType {
    return foo + 1
}

如果这不能做到,我看不出有什么区别

type FooType interface{}

func AddOne(foo FooType) (FooType, error) {
    switch v := foo.(type) {
        case int:
              return v + 1, nil
        case float64:
              return v + 1.0, nil
    }

    return nil, fmt.Errorf("invalid type %T", foo)
}

// versus
type FooType int | float64

func AddOne(foo FooType) FooType {
    switch v := foo.(type) {
        case int:
              return v + 1
        case float64:
              return v + 1.0
    }

    // assumes the compiler knows that there is no other type is 
    // valid and thus this should always returns a value
    // Would the compiler error out on incomplete switch types?
}

@xibz

出于好奇,我可以直接使用 sum 类型还是明确需要将其强制转换为已知类型才能对它执行任何操作? 因为如果我必须不断地将它转换为已知类型,我真的不知道这比接口已经给我们带来了什么好处。

@rogpeppe ,如果我错了,请纠正我🙏
必须始终执行模式匹配(这就是在函数式编程语言中使用 sum 类型时调用“强制转换”的方式)实际上是使用 sum 类型的最大好处之一。 强制开发人员显式处理和类型的所有可能形状是一种防止开发人员使用变量的方法,认为它是给定的类型,而实际上它是不同的类型。 一个夸张的例子是,在 JavaScript 中:

const a = "1" // string "1"
const b = a + 5 // string "15" and not number 6

如果这不能做到,我看不出有什么区别

我想你自己陈述了一些优势,不是吗?

我看到的主要好处是编译时错误检查,因为解组仍然会在运行时发生,这更有可能是您看到传递无效类型的问题。 另一个好处是界面更受限制,我认为这不需要更改语言。

// Would the compiler error out on incomplete switch types?

基于函数式编程语言的功能,我认为这应该是可能的和可配置的👍

@xibz也有性能,因为这可以在编译时与运行时完成,但是希望在我死前一天有泛型。

@xibz

出于好奇,我可以直接使用 sum 类型还是明确需要将其强制转换为已知类型才能对它执行任何操作?

如果该类型的所有成员都共享该方法,则可以对其调用方法。

以你的int | float64为例,结果是什么:

var x int|float64 = int(2)
var y int|float64 = float64(0.5)
fmt.Println(x * y)

它会进行从intfloat64的隐式转换吗? 或者从float64int 。 还是会恐慌?

所以你几乎是对的 - 在大多数情况下,你需要在使用它之前进行类型检查。 我相信这是一个优势,而不是劣势。

顺便说一句,运行时优势可能很重要。 继续使用您的示例类型, [](int|float64)类型的切片不需要包含任何指针,因为可以用几个字节(由于对齐限制可能为 16 个字节)来表示该类型的所有实例,这可能在某些情况下导致显着的性能改进。

@xibz模式匹配在树遍历代码中很有用,您希望在树中查看不止一层。 类型开关一次只能让您查看一层深,因此您需要嵌套它们。

这有点做作,但例如,如果您有一个表达式语法树,要匹配二次方程,您可能会执行以下操作:

match Add(Add(Mult(Const(a), Power(Var(x), 2)), Mult(Const(b), Var(x))), Const(c)) {
  // here a, b, c are bound to the constants and x is bound to the variable name.
  // x must have been the same in both var expressions or it wouldn't match.
}

仅深入一层的简单示例不会显示出很大的差异,但在这里我们将深入到五层,这对于嵌套类型开关来说会非常复杂。 具有模式匹配的语言可以深入多个层次,同时确保您不会错过任何案例。

不过,我不确定它在编译器之外出现了多少。

@xibz
总和类型的一个好处是您和编译器都知道总和中可以存在哪些类型。 这就是本质上的区别。 对于空接口,您将始终需要担心并防止 api 中的误用,因为始终有一个分支,其唯一目的是在用户给您提供您不期望的类型时进行恢复。

由于在编译器中实现 sum 类型似乎没有什么希望,我希望至少有一个标准的注释指令,比如//go:union A | B | Cgo vet提出并支持。

使用标准的方法来声明一个 sum 类型,N 年后就可以知道有多少包正在使用它。

在最近的泛型设计草案中,也许 sum 类型可以与它们相关联。

草案中提出了使用接口而不是契约的想法,并且接口必须支持类型列表:

type Foo interface { 
     int64, int32, int, uint, uint32, uint64
}

虽然这本身不会产生内存压缩的联合,但也许在泛型函数或结构中使用时,它不会被装箱,并且在处理有限类型列表时它至少会提供类型安全。

也许,在类型开关中使用这些特定的接口需要这样的开关是详尽无遗的。

这不是理想的简短语法(例如: Foo | int32 | []Bar ),但确实如此。

在最近的泛型设计草案中,也许 sum 类型可以与它们相关联。

草案中提出了使用接口而不是契约的想法,并且接口必须支持类型列表:

type Foo interface { 
     int64, int32, int, uint, uint32, uint64
}

虽然这本身不会产生内存压缩的联合,但也许在泛型函数或结构中使用时,它不会被装箱,并且在处理有限类型列表时它至少会提供类型安全。

也许,在类型开关中使用这些特定的接口需要这样的开关是详尽无遗的。

这不是理想的简短语法(例如: Foo | int32 | []Bar ),但确实如此。

与我的提议非常相似: https :

type foobar union {
  int
  float
}

@mathieudevos哇,我真的很喜欢那个。

对我来说,最新的泛型提案最大的奇怪之处(实际上是唯一剩下的奇怪之处)是接口中的类型列表。 他们只是不太合适。 然后你会得到一些只能用作类型参数约束的接口,等等......

union概念在我看来非常有效,因为您可以在union中嵌入interface以实现“包括方法和原始类型的约束”。 接口继续按原样运行,并且通过围绕联合定义的语义,它们可以在常规代码中使用,并且陌生感消失了。

// Ordinary interface
type Stringer interface {
    String() string
}

// Type union
type Foobar union {
    int
    float
}

// Equivalent to an interface with a type list
type FoobarStringer interface {
    Stringer
    Foobar
}

// Unions can intersect
type SuperFoo union {
    Foobar
    int
}

// Doesn't compile since these unions can't intersect
type Strange interface {
    Foobar
    union {
        int
        string
    }
}

编辑 - 实际上,刚看到这个 CL: https :

这一变化的主要好处是它为一般人打开了大门
(非约束)使用带有类型列表的接口

...伟大的! 接口变得完全可用作为和类型,它统一了常规和约束使用的语义。 (显然还没有打开,但我认为这是一个很好的目的地。)

我已经打开 #41716 来讨论 sum 类型版本在当前泛型设计草案中的出现方式。

我只是想分享@henryas关于代数数据类型的旧提案。 用提供的用例编写的非常好。
https://github.com/golang/go/issues/21154
不幸的是,它已在同一天被@mvdan关闭,没有任何赞赏。 我很确定那个人真的有这种感觉,因此 gh 帐户上没有进一步的活动。 我为那个人感到难过。

我真的很喜欢#21154。 尽管(因此@mvdan的)评论将其关闭,因为欺骗并没有完全命中,这似乎是另一回事。 在那里重新打开或包括在此处的讨论中?

是的,我真的希望能够以与该问题中描述的类似的方式对一些更高级的业务逻辑进行建模。 类似枚举、受限选项的总和类型以及建议的接受类型(如其他问题)在工具箱中会很棒。 Go 中的业务/域代码目前有时感觉有点笨重。

我唯一的反馈是接口内的type foo,bar看起来有点尴尬和二等,我同意应该在可空和不可空之间进行选择(如果可能的话)。

@ProximaB我不明白你为什么说“gh 帐户上没有进一步的活动”。 此后,他们还创建并评论了许多其他问题,其中许多是关于 Go 项目的。 我看不到任何证据表明他们的活动受到该问题的影响。

此外,我非常同意 Daniel 将这个问题作为对这个问题的欺骗来结束。 我不明白为什么@andig说他们提出了不同的建议。 就我能理解的 #21154 的文本而言,它提出了与我们在这里讨论的完全相同的内容,如果在这个超级线程中的某处已经建议了确切的语法,我一点也不会感到惊讶(语义,就描述,肯定是。多次)。 事实上,我什至会说 Daniels 的结尾被这个问题的长度证明是正确的,因为它已经包含了对 #21154 的相当详细和微妙的讨论,所以重复所有这些将是艰巨和多余的。

我同意并理解将提案作为受骗者关闭可能令人失望。 但我不知道有什么实用的方法可以避免它。 将讨论集中在一个地方似乎对每个参与者都有好处,并且将同一件事的多个问题保持开放,而不对其进行任何讨论,显然毫无意义。

此外,我非常同意 Daniel 将这个问题作为对这个问题的欺骗来结束。 我不明白为什么@andig说他们提出了不同的建议。 就我所理解的#21154 的文本而言,它提出的内容与我们在这里讨论的完全相同

重读这个问题我同意。 似乎我对泛型合同的这个问题感到困惑。 我强烈支持 sum 类型。 我不是故意听起来很刺耳,如果遇到这种情况,请接受我的道歉。

我是一个人,园艺问题有时会很棘手,所以当我犯错时一定要指出:) 但在这种情况下,我确实认为任何特定的总和类型提案都应该像https:/ /github.com/golang/go/issues/19412#issuecomment -701625548

我是一个人,园艺问题有时会很棘手,所以当我犯错时一定要指出:) 但在这种情况下,我确实认为任何特定的总和类型提案都应该像#19412一样从这个线程中分叉出来

@mvdan不是人类。 相信我。 我是他的邻居。 只是在开玩笑。

感谢您的关注。 我对我的建议没有那么执着。 随意破坏、修改和击落它们的任何部分。 我在现实生活中一直很忙,所以我没有机会积极参与讨论。 很高兴知道人们阅读了我的建议并且有些人确实喜欢它们。

最初的目的是允许按域相关性对类型进行分组,它们不一定共享共同的行为,并让编译器强制执行。 在我看来,这只是一个静态验证问题,是在编译过程中完成的。 编译器不需要生成保留类型之间复杂关系的代码。 生成的代码可能会将这些域类型正常视为常规 interface{} 类型。{} 不同之处在于编译器现在在编译时进行额外的静态类型检查。 这基本上是我提案的精髓#21154

@henryas
我想知道 Golang 是否没有使用鸭子类型会使类型之间的关系更加严格,并允许按照您在提案中描述的域相关性对对象进行分组。

@henryas
我想知道 Golang 是否没有使用鸭子类型会使类型之间的关系更加严格,并允许按照您在提案中描述的域相关性对对象进行分组。

它会,但会破坏与 Go 1 的兼容性承诺。如果我们有显式接口,我们可能不需要 sum 类型。 然而,鸭子打字不一定是坏事。 它使某些东西更加轻便和方便。 我喜欢鸭子打字。 这是使用正确的工具来完成工作的问题。

@henryas我同意。 这是一个假设性的问题。 围棋的创造者绝对深切地考虑了所有的起起落落。
另一方面,像验证接口合规性这样的编码指南永远不会出现。
https://github.com/uber-go/guide/blob/master/style.md#verify -interface-compliance

你能在别处进行这个偏离主题的讨论吗? 有很多人订阅了这个问题。
开放接口满意度自 Go 成立以来一直是 Go 的一部分,并且不会改变。

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