Go泛型:对于映射键的类型约束?

huangapple go评论75阅读模式
英文:

Go generics: type constraint for map keys?

问题

在下面的代码中,我定义了一个通用的链表。Go1.18可以使用链表的实例作为映射的键。然而,当取消注释最后一行时,编译失败,出现以下错误:

Cons[int] does not implement comparable

是否有一种更弱的类型约束可以选择那些可以用作键的类型,或者这是故意的,还是编译器的错误?

package main

import "fmt"

type List[X any] interface {
	isList()
}

type Cons[X any] struct {
	Data X
	Next List[X]
}

func (Cons[X]) isList() {}

type Nil[X any] struct{}

func (Nil[X]) isList() {}

func id[X comparable](x X) X { return x }

func main() {
	x := Cons[int]{5, Nil[int]{}}
	m := map[List[int]]string{}
	m[x] = "Hi"        // 成功
	fmt.Println(m[x])  // 输出 "Hi"
	// fmt.Println(id(x)) // 失败
}
英文:

In the code below, I define a generic linked list. Go1.18 is happy to use an instance of the list as a key to a map. However, the last line, when uncommented, doesn't compile; I get the error:

> Cons[int] does not implement comparable

Is there a weaker type constraint I can use that picks out those types that can be used as keys, or is this intended, or is it a compiler bug?

package main

import "fmt"

type List[X any] interface {
	isList()
}

type Cons[X any] struct {
	Data X
	Next List[X]
}

func (Cons[X]) isList() {}

type Nil[X any] struct{}

func (Nil[X]) isList() {}

func id[X comparable](x X) X { return x }

func main() {
	x := Cons[int]{5, Nil[int]{}}
	m := map[List[int]]string{}
	m[x] = "Hi"        // succeeds
	fmt.Println(m[x])  // prints "Hi"
	// fmt.Println(id(x)) // fails
}

答案1

得分: 5

Go 1.20(2023年2月)

comparable是映射键的正确通用约束条件。

所有符合Go规范中可比较性要求的类型,即使比较可能在运行时引发恐慌,也可以满足comparable约束条件。在1.20版本中,您的代码将按预期编译。

这最终解决了先前Go版本中关于规范可比较类型与comparable类型不一致的问题。详细信息请参见下文。

Go 1.18和1.19

预声明的comparable约束条件是映射键的正确约束条件,但它只能由严格可比较的类型实例化,即支持==!=(作为映射键使用的条件)但不会在运行时引发恐慌的类型。这不包括接口<sup>1</sup>

这在这里提到:https://go.dev/ref/spec#Type_constraints

> 预声明的接口类型comparable表示所有可比较的非接口类型的集合。具体而言,如果类型T满足以下条件,则类型T实现了comparable
>
> - T不是接口类型且T支持操作==!= <sup>2</sup>
> - T是接口类型且T的类型集中的每个类型都实现了comparable
>
> 尽管非类型参数的接口可以进行比较(可能导致运行时恐慌),但它们不实现comparable

这是一个重要的注意事项,因为基本接口类型通常支持相等运算符——比较的是它们的动态类型/值。

因此,您的接口List[X]可以直接用作映射键,例如map[List[int]]string{},但它不实现comparable,因为它具有无限的类型集(它没有项,因此任何类型都实现它)。而Cons也不实现它,因为它具有类型为List[X]的字段。对于这种情况,没有更"弱"的约束条件。

请注意,嵌入comparable的约束条件也适用于映射键,因此如果您确实需要在函数体中使用isList()方法,可以定义一个类似这样的约束条件,并使您的作为映射键的列表结构体实现该约束条件,而不是声明一个接口字段:

// 可以用作约束条件
type List interface {
    comparable
    isList() bool
}

<hr>

<sup>1:规范中的引用暗示存在实现comparable的接口类型,但实际上无法使用任何接口来实例化comparable:只有方法的接口具有无限的类型集,并且具有类型项的接口除了作为约束条件外无法在任何地方使用。</sup>

<sup>2:实际上,此规则并未涵盖支持==的非接口类型,例如type S struct { data any },但这些类型仍然无法实例化comparable https://go.dev/play/p/N-pmE0XC-hB。这是规范中的一个错误。</sup>

英文:

Go 1.20 (February 2023)

comparable is the correct catch-all constraint for map keys.

All types that are comparable as per the Go spec, even if the comparison may panic at run time, can satisfy the comparable constraint. Your code will compile as expected in 1.20.

This finally fixes the inconsistency in previous Go version about spec-comparable types vs comparable types. See below for details.

Go 1.18 and 1.19

The predeclared comparable constraint is the correct constraint for map keys, however it can be instantiated only by strictly comparable types, i.e. types that support == and != (condition for being used as map keys) but won't panic at run time. This excludes interfaces<sup>1</sup>.

This is mentioned here: https://go.dev/ref/spec#Type_constraints

> The predeclared interface type comparable denotes the set of all
> non-interface types that are comparable. Specifically, a type T
> implements comparable if:
>
> - T is not an interface type and T supports the operations == and != <sup>2</sup>
> - T is an interface type and each type in T's type set implements comparable
>
> Even though interfaces that are not type parameters can be compared (possibly causing a run-time panic) they do not implement comparable.

This is an important gotcha, because basic interface types normally do support the equality operators — what is compared is their dynamic types/values.

Therefore, your interface List[X] can be used as a map key directly, as in map[List[int]]string{}, but it does not implement comparable because it has an infinite type set (it has no terms, so any type implements it). And Cons doesn’t implement it either because it has a field of type List[X]. There is no "weaker" constraint for this.

Consider that constraints that embed comparable are also valid for map keys, so if you really need the method isList() in the function body, you can define a constraint like this, and have your lists-that-are-map-key structs implement that, instead of declaring an interface field:

// may use this as a constraint
type List interface {
    comparable
    isList() bool
}

<hr>

<sup>1: the quote from the specs hints there are interface types that implement comparable, but it's effectively not possible to instantiate comparable with any interface at all: interfaces with only methods have an infinite type set, and interfaces with type terms can't be used anywhere except as constraints.</sup>

<sup>2: this rule actually doesn't cover non-interface types that support ==, like type S struct { data any }, but these types still can't instantiate comparable https://go.dev/play/p/N-pmE0XC-hB. This is a bug in the spec.</sup>

huangapple
  • 本文由 发表于 2022年3月17日 07:55:58
  • 转载请务必保留本文链接:https://go.coder-hub.com/71505556.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定