原文地址:https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-type-parameters.md
摘要
我们建议扩展Go语言,以便为类型和函数添加可选的类型参数。类型参数可能受接口类型的约束。我们还建议扩展用作类型约束的接口类型,允许列出可能分配给它们的类型集。在许多情况下,可以支持通过统一算法进行类型推断,从而可以从函数调用中省略类型实参。该设计与Go 1 完全向后兼容。
如何阅读本文
这篇文章很长。下面是一些有关如何阅读的指南。
- 我们从一个高层次的概述开始,非常简要地描述这些概念。
- 然后,我们从头开始解释整个设计,并通过简单的示例介绍我们需要的细节。
- 完整描述设计之后,我们将讨论实现,设计中的一些问题以及与其他泛型方法的比较。
- 然后,我们提供有关如何在实践中使用此设计的几个完整示例。
- 在示例之后,附录中讨论了一些次要细节。
高度概括
本节非常简短地解释了设计草案建议的更改。本部分适用于已经熟悉泛型如何在Go语言中工作的人们。这些概念将在后续各节中详细说明。
- 函数可以使用关键字
type
引入的其他类型参数列表:func F(type T)(p T) { ... }
。 - 这些类型参数可以作为常规参数在函数体内使用。
- 类型也可以具有类型参数列表:
type M(type T) []T
。 - 每个类型参数可以具有可选的类型约束:
func F(type T Constraint)(p T) { ... }
- 类型约束是接口类型。
- 用作类型约束的接口类型可以拥有预先声明的类型的列表。只有其基础类型是这些类型之一的类型才能实现该接口。
- 使用泛型函数或类型需要传递类型实参。
- 在常见情况下,可以通过类型推断省略类型实参。
- 如果类型参数具有类型约束,则其类型实参必须实现此接口。
- 泛型函数只能使用类型约束所允许的操作。
在以下各节中,我们将详细介绍每种更改。你也可以跳过这些直接查看示例。
背景
这个版本的设计草案与2019年7月31日提出的版本有很多相似之处,但协议已被删除并由接口类型代替。
有许多要求为Go添加泛型支持的请求。在issue和在线文档中已经进行了广泛的讨论。
有几种添加类型参数的建议,可以通过上面的链接找到。这里提出的许多想法以前都曾出现过。此处描述的主要新特性是语法和对作为约束的接口类型的仔细校准。
此设计草案建议扩展Go语言,添加一种参数多态的形式,其中类型参数不受声明的子类型关系(如在某些面向对象的语言中)的约束,但受显式定义的约束结构的约束。
此设计不支持模板编程或任何其他形式的编译时处理。
由于术语“泛型”在Go社区中得到了广泛使用,因此我们将在下文中将其用作表示具有类型参数的函数或类型的简写。不要将本设计中使用的泛型(generic)一词与其他语言(例如C ++,C#,Java或Rust)中的同一术语混淆;它们有相似之处,但不相同。
设计
我们将基于简单的示例分阶段描述完整的设计。
类型参数
泛型代码是使用稍后将指定的类型编写的代码。未指定的类型称为类型参数。运行泛型代码时,type形参将设置为type实参。
这是一个打印出切片的每个元素的函数,其中切片的元素类型(此处称为T
)是未知的。这是我们要允许以支持泛型编程的函数类型的一个简单例子。(稍后我们还将讨论泛型类型)。
// Print prints the elements of a slice.
// It should be possible to call this with any slice value.
func Print(s []T) { // Just an example, not the suggested syntax.
for _, v := range s {
fmt.Println(v)
}
}
使用这种方法,首先要做出的决定是:如何声明T
类型参数?在Go之类的语言中,我们希望每个标识符都以某种方式声明。
在这里,我们进行的设计决策是:类型参数与普通的非类型函数参数相似,因此应与其他参数一起列出。但是,类型参数与非类型参数不同,因此,尽管它们出现在参数列表中,但我们还是要加以区分。因此我们做出以下设计决策:我们定义一个附加的,可选的参数列表,描述类型参数。该参数列表出现在常规参数之前。它以关键字type
开头,并列出类型参数。
// Print prints the elements of any slice.
// Print has a type parameter T, and has a single (non-type)
// parameter s which is a slice of that type parameter.
func Print(type T)(s []T) {
// same as above
}
这表示在函数Print
中,标识符T
是一个类型参数,一种当前未知的类型,但在调用该函数时将是已知的。如上所述,当描述普通的非类型参数时,类型参数可以用作类型。它也可以在函数体内使用。
由于Print
具有类型参数,因此任何调用都Print
必须提供类型实参。稍后,我们将看到通常如何通过使用函数参数类型推断来使用非类型参数推导出该类型参数。现在,我们将显式传递type参数。类型实参的传递与声明类型形参的传递非常相似:作为单独的参数列表。在调用时不使用type
关键字。
// Call Print with a []int.
// Print has a type parameter T, and we want to pass a []int,
// so we pass a type argument of int by writing Print(int).
// The function Print(int) expects a []int as an argument.
Print(int)([]int{1, 2, 3})
// This will print:
// 1
// 2
// 3
约束条件
让我们的例子稍微复杂一些。让我们将其转换为一个函数,该函数可以将任意类型的切片转换为[]string。
// This function is INVALID.
func Stringify(type T)(s []T) (ret []string) {
for _, v := range s {
ret = append(ret, v.String()) // INVALID
}
return ret
}
乍一看似乎不错,但在此示例v为type T,而我们对T一无所知。特别是,我们不知道T有String方法。因此,v.String()的调用无效。
当然,在支持泛型编程的其他语言中也会出现相同的问题。例如,在C ++中,泛型函数(用C ++术语来说是函数模板)可以对泛型类型的值调用任何方法。也就是说,在C ++方法中,调用v.String()
就可以了。如果使用没有String方法的类型实参调用该函数,则在编译v.String时将会报错。这些错误可能很长,因为在错误发生之前可能有几层函数调用,为了了解出了什么问题必须报告所有这些层的异常。
对于Go语言,C ++方法将是一个糟糕的选择。原因之一是语言的风格。在Go语言中,我们不会进行名称的引用,例如在这种情况下,是希望String存在。Go会将所有看到的名称解析为其声明。
另一个原因是Go旨在支持大规模编程。我们必须考虑以下情况:泛型函数定义(Stringify)和对泛型函数的调用(未显示,但可能在其他软件包中)相距甚远。通常,所有泛型代码都希望类型实参满足某些要求。我们将这些要求称为约束(其他语言也有类似的想法,称为类型界限或特征界限或其他的概念)。在这种情况下,约束非常明显:类型必须具有String() string
方法。在其他情况下,它可能不那么明显。
我们不想从Stringify
的任何使用中得出约束(在这种情况下,调用String方法)。如果这样做,对Stringify
的微小更改可能会更改约束。这意味着较小的更改可能导致调用该函数的代码变更而意外中断。Stringify
故意更改其约束并强迫用户进行更改也是一种方法。我们要避免的是意外更改Stringify
约束。
这意味着约束必须对调用者传递的类型实参和泛型函数中的代码都设置限制。调用者只能传递满足约束的类型参数。泛型函数只能以约束所允许的方式使用这些值。我们认为这是一条重要规则,适用于在Go中定义泛型编程的任何尝试:泛型代码只能使用已知其类型参数实现的操作。
允许任何类型的操作
在进一步讨论约束之前,让我们简要地指出在没有约束的情况下会发生什么。如果泛型函数没有为类型参数指定约束(如上述Print
方法那样),则该参数允许使用任何类型参数。泛型函数与该类型参数的值一起使用的操作只能是允许用于任何类型的值的那些操作。在上面的示例中,Print
函数声明了一个变量v
,类型为类型参数T
,并将该变量传递给函数。
允许任何类型的操作是:
- 声明那些类型的变量
- 将相同类型的其他值分配给这些变量
- 将这些变量传递给函数或从函数返回它们
- 获取那些变量的地址
- 将这些类型的值转换或赋值给类型
interface{}
- 将类型
T
的值转换为类型T
(允许但无用) - 使用类型断言将接口值转换为类型
- 在类型switch中将类型用作case
- 定义和使用这些类型的复合类型,例如该类型的切片
- 将类型传递给一些内置函数,例如
new
尽管目前预计不会进行任何修改,但将来的语言更改可能还会添加其他此类操作。
定义约束
Go已经具有与我们需要的约束接近的构造:接口类型(interface)。接口类型是一组方法。可以分配给接口类型的变量的是那些实现相同方法的类型的值。只能使用接口类型执行的操作是调用方法。
使用类型参数调用泛型函数类似于分配给接口类型的变量:类型参数必须实现类型参数的约束。编写泛型函数就像使用接口类型的值一样:泛型代码只能使用约束所允许的操作(或任何其他类型所允许的操作)。
在这种设计中,约束只是接口类型。实现约束只是实现接口类型。(稍后,我们将看到如何为方法定义约束)。
对于Stringify
示例,我们需要一个接口类型,该接口类型的String
方法不带任何参数并返回type的值string
。
// Stringer is a type constraint that requires the type argument to have
// a String method and permits the generic function to call String.
// The String method should return a string representation of the value.
type Stringer interface {
String() string
}
(此讨论无关紧要,但这定义了与标准库的fmt.Stringer
类型相同的接口,实际代码可能只使用fmt.Stringer
。)
使用约束
对于泛型函数,可以将约束视为类型实参的类型:元类型。因此,尽管不需要泛型函数使用约束,但泛型函数在使用时会在类型参数列表中作为类型参数的元类型列出。
// Stringify calls the String method on each element of s,
// and returns the results.
func Stringify(type T Stringer)(s []T) (ret []string) {
for _, v := range s {
ret = append(ret, v.String())
}
return ret
}
在这种情况下,单个类型参数T
后跟适用的约束。在这个例子里就是Stringer
。
多种类型参数
尽管该Stringify
示例仅使用单个类型参数,但函数可能具有多个类型参数。
// Print2 has two type parameters and two non-type parameters.
func Print2(type T1, T2)(s1 []T1, s2 []T2) { ... }
比较一下
// Print2Same has one type parameter and two non-type parameters.
func Print2Same(type T)(s1 []T, s2 []T) { ... }
在Print2
中 s1
和s2
可以是不同类型的切片。Print2Same
中 s1和s2必须是相同元素类型的切片。
每个类型参数可能都有其自己的约束。
// Stringer is a type constraint that requires a String method.
// The String method should return a string representation of the value.
type Stringer interface {
String() string
}
// Plusser is a type constraint that requires a Plus method.
// The Plus method is expected to add the argument to an internal
// string and return the result.
type Plusser interface {
Plus(string) string
}
// ConcatTo takes a slice of elements with a String method and a slice
// of elements with a Plus method. The slices should have the same
// number of elements. This will convert each element of s to a string,
// pass it to the Plus method of the corresponding element of p,
// and return a slice of the resulting strings.
func ConcatTo(type S Stringer, P Plusser)(s []S, p []P) []string {
r := make([]string, len(s))
for i, v := range s {
r[i] = p[i].Plus(v.String())
}
return r
}
如果为任何类型参数指定了约束,则每个类型参数都必须具有约束。如果某些类型参数需要约束而有些则不需要,则那些不需要的需要有一个interface{}
约束。
// StrAndPrint takes a slice of labels, which can be any type,
// and a slice of values, which must have a String method,
// converts the values to strings, and prints the labelled strings.
func StrAndPrint(type L interface{}, T Stringer)(labels []L, vals []T) {
// Stringify was defined above. It returns a []string.
for i, s := range Stringify(vals) {
fmt.Println(labels[i], s)
}
}
单个约束可用于多个类型参数,就像单个类型可用于多个非类型函数参数一样。约束分别应用于每个类型参数。
// Stringify2 converts two slices of different types to strings,
// and returns the concatenation of all the strings.
func Stringify2(type T1, T2 Stringer)(s1 []T1, s2 []T2) string {
r := ""
for _, v1 := range s1 {
r += v1.String()
}
for _, v2 := range s2 {
r += v2.String()
}
return r
}
泛型类型
我们不仅需要泛型函数:我们还需要泛型类型。我们建议将类型扩展为可接受类型参数。
// Vector is a name for a slice of any element type.
type Vector(type T) []T
类型的参数就像函数的类型参数一样。
在类型定义内,可以像其他类型一样使用。
要使用泛型类型,必须提供类型参数。这看起来像一个函数调用,只是在这种情况下该函数实际上是一个类型。这称为实例化。当通过为类型参数提供类型实参来实例化类型时,我们会生成一个类型,其中类型定义中对类型参数的每次使用都将被相应的类型实参替换。
// v is a Vector of int values.
//
// This is similar to pretending that "Vector(int)" is a valid identifier,
// and writing
// type "Vector(int)" []int
// var v "Vector(int)"
// All uses of Vector(int) will refer to the same "Vector(int)" type.
//
var v Vector(int)
泛型类型可以具有方法。方法的接收类型必须声明与接收类型的定义中声明的数量相同的类型参数。声明它们时没有type
关键字或任何约束。
// Push adds a value to the end of a vector.
func (v *Vector(T)) Push(x T) { *v = append(*v, x) }
方法声明中列出的类型参数不必与类型声明中的类型参数具有相同的名称。特别是如果方法未使用它们,则可以使用_
。
如果一个类型可以引用自身,则泛型类型也可以引用自身,但是当这样做时,类型实参必须是按相同顺序列出的类型形参。此限制可防止类型实例化的无限递归。
// List is a linked list of values of type T.
type List(type T) struct {
next *List(T) // this reference to List(T) is OK
val T
}
// This type is INVALID.
type P(type T1, T2) struct {
F *P(T2, T1) // INVALID; must be (T1, T2)
}
此限制适用于直接和间接引用。
// ListHead is the head of a linked list.
type ListHead(type T) struct {
head *ListElement(T)
}
// ListElement is an element in a linked list with a head.
// Each element points back to the head.
type ListElement(type T) struct {
next *ListElement(T)
val T
// Using ListHead(T) here is OK.
// ListHead(T) refers to ListElement(T) refers to ListHead(T).
// Using ListHead(int) would not be OK, as ListHead(T)
// would have an indirect reference to ListHead(int).
head *ListHead(T)
}
(注意:在了解人们如何编写代码的情况下,可以放宽此规则,以允许某些情况下使用不同的类型参数。)
泛型类型的类型参数可能具有约束。
// StringableVector is a slice of some type, where the type
// must have a String method.
type StringableVector(type T Stringer) []T
func (s StringableVector(T)) String() string {
var sb strings.Builder
for i, v := range s {
if i > 0 {
sb.WriteString(", ")
}
// It's OK to call v.String here because v is of type T
// and T's constraint is Stringer.
sb.WriteString(v.String())
}
return sb.String()
}
不支持泛型方法
尽管泛型类型的方法可以使用该类型的参数,但是方法本身没有附加的类型参数。
在后面的[issues]章节对此进行了更多讨论。
操作符
如我们所见,我们将接口类型用作约束。接口类型提供了一组方法,仅此而已。这意味着到目前为止,我们已经看到,泛型函数可以对类型参数的值执行的唯一操作(对任何类型都允许的操作除外)是调用方法。
但是,对于我们要表达的所有内容,仅靠方法调用还不够。考虑下面这个简单的函数,该函数返回值切片的最小元素,其中该切片被假定为非空。
// This function is INVALID.
func Smallest(type T)(s []T) T {
r := s[0] // panic if slice is empty
for _, v := range s[1:] {
if v < r { // INVALID
r = v
}
}
return r
}
任何合理的泛型实现都会让你编写一个类似的函数。问题是表达式v < r
。这假定了T
支持<
运算符,但T
没有约束。在没有约束的情况下,该函数Smallest
只能使用可用于所有类型的操作,但是并不是所有的Go类型都支持操作<
。不幸的是,由于<
这不是一种方法,因此没有明显的方法来为<
编写允许的约束(接口类型)。
我们需要写一种仅接受支持<
类型的约束的方法。对此,我们注意到,除了稍后将要讨论的两个例外,语言定义的所有算术,比较和逻辑运算符都只能与该语言预先声明的类型或已定义的类型一起使用。其基础类型是那些预先声明的类型之一。也就是说,运算符<
只能与预先声明的类型(例如int
或float64
)一起使用,或者只能将其基础类型为这些类型之一的已定义类型使用。Go不允许<
与复合类型或任意定义的类型一起使用。
这意味着我们不必尝试为<
编写约束,而是可以采用另一种方法:与其说约束应支持哪些运算符,不如说约束应接受哪些(底层)类型。
约束中的类型列表
用作约束的接口类型可以列出可以用作类型参数的显式类型。这是通过使用type
关键字后跟逗号分隔的类型列表来完成的。
例如:
// SignedInteger is a type constraint that permits any
// signed integer type.
type SignedInteger interface {
type int, int8, int16, int32, int64
}
SignedInteger
限制规定类型参数必须是所列出的类型之一。更准确地说,类型实参的基础类型必须与类型列表中类型之一的基础类型相同。这意味着SignedInteger
将接受列出的整数类型,并且还将接受定义为这些类型之一的任何类型。
当泛型函数使用具有这些约束的类型参数时,它可以使用所有列出的类型允许的任何操作。这可以像操作<
,range
,<-
,等等。如果可以使用约束中列出的每种类型成功编译函数,则允许该操作。
一个约束可能只有一个类型列表。
对于前面显示的Smallest
示例,我们可以使用如下约束:
package constraints
// Ordered is a type constraint that matches any ordered type.
// An ordered type is one that supports the <, <=, >, and >= operators.
type Ordered interface {
type int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr,
float32, float64,
string
}
在实践中,很可能会定义此约束并将其导出到新的标准库包constraints
中,以便函数和类型定义可以使用它。
给定该约束,我们就可以编写此函数,下面这个函数现在有效:
// Smallest returns the smallest element in a slice.
// It panics if the slice is empty.
func Smallest(type T constraints.Ordered)(s []T) T {
r := s[0] // panics if slice is empty
for _, v := range s[1:] {
if v < r {
r = v
}
}
return r
}
约束中的可比较类型
前面我们提到,有两个例外的规则,即运算符只能与语言预先声明的类型一起使用。==
和!=
是例外,结构,数组和接口类型均允许进行比较操作。如果我们希望能够编写一个接受任何可比较类型的约束,知道这一点就很有用了。
为此,我们引入了一个新的预先声明的类型约束:comparable
。具有comparable
约束的类型参数接受任何可比较类型作为实参。它允许使用==
,!=
与该类型参数的值进行比较。
例如,可以使用任何可比较的类型实例化下面这个函数:
// Index returns the index of x in s, or -1 if not found.
func Index(type T comparable)(s []T, x T) int {
for i, v := range s {
// v and x are type T, which has the comparable
// constraint, so we can use == here.
if v == x {
return i
}
}
return -1
}
与所有约束一样,由于comparable
是接口类型,因此可以将其嵌入用作约束的另一种接口类型中:
// ComparableHasher is a type constraint that matches all
// comparable types with a Hash method.
type ComparableHasher interface {
comparable
Hash() uintptr
}
约束ComparableHasher
可以通过任何可比较类型实现,也可以使用一种Hash() uintptr
方法来实现。ComparableHasher
用作约束的泛型函数可以比较该类型的值并可以调用该Hash
方法。
接口类型中的类型列表
具有类型列表的接口类型只能用作对类型参数的约束。它们可能不能用作普通接口类型。预先声明的接口类型comparable
也是如此。
在将来的语言版本中可能会取消此限制。具有类型列表的接口类型可用作求和类型的一种形式,尽管它可以具有value nil
。可能需要一些替代语法来匹配相同类型而不是基础类型。也许会是type ==
。不过目前这是不允许的。
函数参数类型推断
在许多情况下,当调用带有类型参数的函数时,我们可以使用interface
来避免显式写出类型参数。
回顾之前的Print
方法:
Print(int)([]int{1, 2, 3})
类型参数int
中的函数调用可以从非类型参数的类型来推断。
仅当所有函数的类型参数都用于函数(非类型)输入参数的类型时,才可以这样做。如果有一些类型参数仅用于函数的结果参数类型,或者仅在函数体中使用,我们的算法无法推断函数的类型参数,因为没有值可用来推断类型。
当可以推断函数的类型参数时,语言便可以使用统一类型。在调用方,我们有实际(非类型)参数的类型列表,在Print
例中为[]int
。在函数这边是类型函数的无类型参数,这对于Print
是[]T
。在列表中,我们丢弃函数侧不使用的类型参数的各个参数。然后,我们必须统一其余的参数类型。
类型统一是一种two-pass算法。在第一遍中,我们忽略了调用方的无类型化常量及其在函数定义中的对应类型。
我们在列表中比较相应的类型。它们的结构必须相同,除非函数一侧的类型参数与出现在调用者一侧的类型参数相匹配。如果同一个类型参数在函数侧出现多次,它将与调用方侧的多个参数类型匹配。这些调用者类型必须相同,否则类型统一失败我们将报告错误。
第一次通过后,我们在调用方检查所有未定义类型的常量。如果没有无类型的常量,或者相应函数类型中的类型参数与其他输入类型匹配,则类型统一完成。
否则,对于第二遍,对于尚未设置相应函数类型的任何未类型化的常量,我们将以默认的方式确定未类型常量的默认类型。然后我们再次运行类型统一算法,这次没有任何未类型化的常量。
在这个例子中
s1 := []int{1, 2, 3}
Print(s1)
我们比较[]int
和[]T
,T
匹配int
。单一类型参数T
是int
,因此我们推断对Print
的调用实际上是对Print(int)
的调用。
对于更复杂的示例,请考虑
// Map calls the function f on every element of the slice s,
// returning a new slice of the results.
func Map(type F, T)(s []F, f func(F) T) []T {
r := make([]T, len(s))
for i, v := range s {
r[i] = f(v)
}
return r
}
这两个类型参数F
和T
都用于输入参数,因此可以进行类型推断。在调用中
strs := Map([]int{1, 2, 3}, strconv.Itoa)
我们将[]int
与[]F
统一,F
匹配int
。我们统一strconv.Itoa
的类型,即func(int) string
与func(F) T
,F
与int
以及T
与string
。类型参数F
匹配两次,两次都匹配int
。匹配成功,因此Map
的调用是调用Map(int, string)
。
要查看有效的无类型常量规则,请考虑:
// NewPair returns a pair of values of the same type.
func NewPair(type F)(f1, f2 F) *Pair(F) { ... }
在上面的调用中,NewPair(1, 2)
两个参数都是未类型化的常量,因此在第一遍中都将忽略。第一次通过后就没有什么可以统一的了,此时我们还有两个未类型化的常量。两者均设置为其默认类型int
。类型统一的第二次运行用int
匹配F
,所以最终调用NewPair(int)(1, 2)
。
在NewPair(1, int64(2))
调用中,第一个参数是无类型的常量,因此我们在第一遍中将其忽略。然后我们将int64
与F
统一。此时,未类型化常量相对应的类型参数已完全确定,因此最终调用为NewPair(int64)(1, int64(2))
。
在NewPair(1, 2.5)
调用中,两个参数都是未类型化的常量,因此我们继续进行第二遍。这次我们将第一个常数设置为int
,将第二个常数设置为float64
。然后,我们尝试F
与int
和float64
统一,统一失败,因此我们报告编译错误。
注意,类型推断是在不考虑约束的情况下完成的。首先,我们使用类型推断来确定要用于该函数的类型参数,然后,如果成功,则检查这些类型参数是否实现了约束(如果有)。
请注意,在成功进行类型推断之后,对于任何函数调用,编译器仍必须检查是否参数分配是否合理。
(注意:类型推断是一种便利功能。尽管我们认为这是一项重要功能,但它并未为设计添加任何功能,只是为使用提供了便利。可以从初始实现中将其省略,并查看是否被需要,也就是此功能不需要其他语法,并且可以生成更具可读性的代码。)
使用约束中引用自己的类型
对于泛型函数来说,要求类型自变量及其方法本身就是类型的方法可能是有用的。例如这在比较方法中自然会出现。(请注意,我们在这里讨论的是方法,而不是运算符。)假设我们要编写一个Index
方法,该方法使用一种Equal
方法来检查它是否找到了所需的值。我们想这样写:
// Index returns the index of e in s, or -1 if not found.
func Index(type T Equaler)(s []T, e T) int {
for i, v := range s {
if e.Equal(v) {
return i
}
}
return -1
}
为了编写Equaler
约束,我们必须编写一个可以引用传入的类型实参的约束。我们无法直接实现,但是我们可以做的是编写一个使用类型形参的接口类型。
// Equaler is a type constraint for types with an Equal method.
type Equaler(type T) interface {
Equal(T) bool
}
为此,Index
将类型参数T
传递给Equaler
。规则是如果类型约束具有单个类型参数,并且在没有显式类型参数的情况下用于函数的类型参数列表,则这个类型参数是要约束的类型参数。换句话说,在上面的Index
定义中,约束Equaler
被视为Equaler(T)
。
此版本的Index
可以与此处定义的equalInt
类型一起使用:
// equalInt is a version of int that implements Equaler.
type equalInt int
// The Equal method lets equalInt implement the Equaler constraint.
func (a equalInt) Equal(b equalInt) bool { return a == b }
// indexEqualInts returns the index of e in s, or -1 if not found.
func indexEqualInt(s []equalInt, e equalInt) int {
return Index(equalInt)(s, e)
}
在此示例中,当传递equalInt
给Index
时,我们检查是否equalInt
实现了Equaler
约束。由于Equaler
有一个类型参数,我们传递Index
的类型参数到Equaler
,equalInt
作为类型实参。这样,该约束Equaler(equalInt)
可以由任何类型的Equal(equalInt) bool
的方法满足。equalInt
类型具有一个Equal
接受类型参数的方法equalInt
,因此一切都满足要求,并且编译成功。
类型参数的相互引用
在单个类型参数列表中,约束可以引用任何其他的类型参数,即使是稍后在同一列表中声明的参数也是如此。(类型参数的范围从参数列表的type
关键字开始,并扩展到封闭函数或类型声明的末尾。)
例如,考虑一个关于图的类包,其中包含可用于图的通用算法。该算法使用两种类型的,Node
和Edge
。Node有一种方法Edges() []Edge
。Edge
有一种方法Nodes() (Node, Node)
。图可以表示为[]Node
。
这种简单的表示足以实现图算法,例如找到最短路径。
package graph
// NodeConstraint is the type constraint for graph nodes:
// they must have an Edges method that returns the Edge's
// that connect to this Node.
type NodeConstraint(type Edge) interface {
Edges() []Edge
}
// EdgeConstraint is the type constraint for graph edges:
// they must have a Nodes method that returns the two Nodes
// that this edge connects.
type EdgeConstraint(type Node) interface {
Nodes() (from, to Node)
}
// Graph is a graph composed of nodes and edges.
type Graph(type Node NodeConstraint(Edge), Edge EdgeConstraint(Node)) struct { ... }
// New returns a new graph given a list of nodes.
func New(
type Node NodeConstraint(Edge), Edge EdgeConstraint(Node)) (
nodes []Node) *Graph(Node, Edge) {
...
}
// ShortestPath returns the shortest path between two nodes,
// as a list of edges.
func (g *Graph(Node, Edge)) ShortestPath(from, to Node) []Edge { ... }
这里有很多类型参数和实例化。在Graph
中Node
的约束,被传递给类型约束NodeConstraint
的Edge
是Graph
的第二个类型参数。NodeConstraint
用类型参数实例化Edge
,因此我们看到Node
必须有一个返回Edges
的切片的方法Edge
,这就是我们想要的。这个约束同样适用Edge
,对于New
函数对是重复相同的类型参数和约束。我们并不是说这很简单,而是我们认为这是可能的。
值得注意的是,乍一看,这看起来像是接口类型的典型用法,Node
和Edge
是具有特定方法的非接口类型。为了使用graph.Graph
,用于Node
和Edge
的类型实参必须定义遵循确定模式的方法,但实际上不必使用接口类型。特别是,这些方法不返回接口类型。
例如,在其他软件包中考虑以下类型定义:
// Vertex is a node in a graph.
type Vertex struct { ... }
// Edges returns the edges connected to v.
func (v *Vertex) Edges() []*FromTo { ... }
// FromTo is an edge in a graph.
type FromTo struct { ... }
// Nodes returns the nodes that ft connects.
func (ft *FromTo) Nodes() (*Vertex, *Vertex) { ... }
此处没有接口类型,但是我们可以使用类型实参*Vertex
和*FromTo
实例化graph.Graph
。
var g = graph.New(*Vertex, *FromTo)([]*Vertex{ ... })
*Vertex
和*FromTo
不是接口类型,但是当一起使用时,它们定义实现graph.Graph
约束的方法。请注意,我们不能将Vertex
或FromTo
传递给graph.New
,因为Vertex
和FromTo
没有实现约束。在指针类型*Vertex
和*FromTo
定义了Edges
和Nodes
的方法; 类型Vertex
,FromTo
没有任何方法。
当使用泛型接口类型作为约束时,我们首先使用类型参数列表中提供的类型实参实例化该类型,然后将对应的类型实参与实例化的约束进行比较。在此示例中,to graph.New
的类型实参Node
具有一个约束NodeConstraint(Edge)
。当我们使用Node
类型实参*Vertex
和Edge
类型实参*FromTo
进行graph.New
调用时,为了检查对Node
的约束,编译器必须使用类型实参*FromTo
实例化NodeConstraint
。这就产生了一个实例化的约束,在这种情况下,该约束是拥有Edges() []*FromTo
的Node
,并且编译器将验证*Vertex
是否满足约束。
虽然Node
和Edge
没有使用接口类型进行实例化,如果你喜欢shi使用接口类型也可以。
type NodeInterface interface { Edges() []EdgeInterface }
type EdgeInterface interface { Nodes() (NodeInterface, NodeInterface) }
我们可以使用类型NodeInterface
和EdgeInterface
实例化graph.Graph
,因为它们实现了类型约束。没有太多理由以这种方式实例化类型,但这是允许的。
类型参数引用其他类型参数的能力说明了一个重要的观点:任何尝试向Go添加泛型的要求都应该是可以实例化具有多个类型参数的泛型代码,这些类型参数以相互引用,编译器可以检查。
指针方法
在某些情况下,仅当类型实参A
具有在指针类型*A
上定义的方法时,泛型函数才能按预期工作。在编写期望修改调用值的方法的泛型函数时会遇到这种情况。
考虑下面函数的示例,该函数期望一种类型T
,该类型具有一种基于字符串初始化值的Set(string)
方法。
// Setter is a type constraint that requires that the type
// implement a Set method that sets the value from a string.
type Setter interface {
Set(string)
}
// FromStrings takes a slice of strings and returns a slice of T,
// calling the Set method to set each returned value.
//
// Note that because T is only used for a result parameter,
// type inference does not work when calling this function.
// The type argument must be passed explicitly at the call site.
//
// This example compiles but is unlikely to work as desired.
func FromStrings(type T Setter)(s []string) []T {
result := make([]T, len(s))
for i, v := range s {
result[i].Set(v)
}
return result
}
现在,让我们看看其他程序包中的一些代码(此示例无效)。
// Settable is a integer type that can be set from a string.
type Settable int
// Set sets the value of *p from a string.
func (p *Settable) Set(s string) {
i, _ := strconv.Atoi(s) // real code should not ignore the error
*p = Settable(i)
}
func F() {
// INVALID
nums := FromStrings(Settable)([]string{"1", "2"})
// Here we want nums to be []Settable{1, 2}.
...
}
我们希望是用FromStrings
来获取[]Settable
的切片。不幸的是,该示例无效,无法编译。
问题是FromStrings
需要具有Set(string)
方法的类型。函数F
是试图通过Settable
实例化FromStrings
,但Settable
没有Set
方法。有Set
方法的类型是*Settable
。
因此,让我们使用*Settable
改写F
。
func F() {
// Compiles but does not work as desired.
// This will panic at run time when calling the Set method.
nums := FromStrings(*Settable)([]string{"1", "2"})
...
}
这可以成功编译,但不幸的是它将在运行时抛出异常。问题是FromStrings
创建了[]T
切片。当使用*Settable
实例化时,表示类型为 []*Settable
。当FromStrings
调用result[i].Set(v)
时,会将存储在result[i]
中的指针传递给Set
方法。那个指针是nil
。Settable.Set
方法将由nil
接收方调用,并导致空指针异常。
我们需要的是写一种FromStrings
方法,使得它可以将类型Settable
作为参数但可以调用指针方法。强调一遍,我们不能使用Settable
因为它没有Set
方法,我们也不能使用*Settable
因为那样我们就不能创建Settable
切片。
一种可行的方法是使用两个不同的类型参数:Settable
和*Settable
。
package from
// Setter2 is a type constraint that requires that the type
// implement a Set method that sets the value from a string,
// and also requires that the type be a pointer to its type parameter.
type Setter2(type B) interface {
Set(string)
type *B
}
// FromStrings2 takes a slice of strings and returns a slice of T,
// calling the Set method to set each returned value.
//
// We use two different type parameters so that we can return
// a slice of type T but call methods on *T.
// The Setter2 constraint ensures that PT is a pointer to T.
func FromStrings2(type T interface{}, PT Setter2(T))(s []string) []T {
result := make([]T, len(s))
for i, v := range s {
// The type of &result[i] is *T which is in the type list
// of Setter2, so we can convert it to PT.
p := PT(&result[i])
// PT has a Set method.
p.Set(v)
}
return result
}
我们可以这样调用FromStrings2
:
func F2() {
// FromStrings2 takes two type parameters.
// The second parameter must be a pointer to the first.
// Settable is as above.
nums := FromStrings2(Settable, *Settable)([]string{"1", "2"})
// Now nums is []Settable{1, 2}.
...
}
这种方法可以正常工作,但是很尴尬。它通过强制传递两个类型参数解决FromStrings2
。第二个类型参数必须是第一个类型参数的指针。这是一个复杂的要求,因为看起来应该是一个相当简单的情况。
另一种方法是传递函数而不是调用方法。
// FromStrings3 takes a slice of strings and returns a slice of T,
// calling the set function to set each returned value.
func FromStrings3(type T)(s []string, set func(*T, string)) []T {
results := make([]T, len(s))
for i, v := range s {
set(&results[i], v)
}
return results
}
我们可以像下面这样调用Strings3
:
func F3() {
// FromStrings3 takes a function to set the value.
// Settable is as above.
nums := FromStrings3(Settable)([]string{"1", "2"},
func(p *Settable, s string) { p.Set(s) })
// Now nums is []Settable{1, 2}.
}
这种方法也可以按预期工作,但也很尴尬。调用者必须传递一个函数才能调用Set
方法。这是我们在使用泛型时希望避免的样板代码。
尽管这些方法很尴尬,但它们确实有效。就是说,我们建议另一个解决此类问题的功能:一种表示对类型参数的指针(而不是对类型参数本身)的约束的方法。完成此操作的方法是将type参数写为指针类型:(type *T Constraint)。
在类型参数列表中写*T
而不是T
会改变两件事。假设调用的类型参数为A
,并且约束为Constraint
(可以使用此语法而没有约束,但是没有理由这样做)。
更改的第一件事Constraint
是应用于*A
而不是A
。即*A
必须实现Constraint
。A
可以实现Constraint
,但是要求是*A
实现它。请注意,如果Constraint
有任何方法,则意味着A
一定不能是指针类型:如果A
是指针类型,则*A
是指向指针的指针,并且此类永远不会有任何方法。
更改的第二件事是,在函数体内的任何方法Constraint
都被视为指针方法。只能在type的*T
值或type的可寻址值上调用它们T
。
// FromStrings takes a slice of strings and returns a slice of T,
// calling the Set method to set each returned value.
//
// We write *T, meaning that given a type argument A,
// the pointer type *A must implement Setter.
//
// Note that because T is only used for a result parameter,
// type inference does not work when calling this function.
// The type argument must be passed explicitly at the call site.
func FromStrings(type *T Setter)(s []string) []T {
result := make([]T, len(s))
for i, v := range s {
// result[i] is an addressable value of type T,
// so it's OK to call Set.
result[i].Set(v)
}
return result
}
同样,使用*T
意味着给定类型参数A
,该类型*A
必须实现约束Setter
。在这种情况下,Set
必须位于*A
中。在内部FromStrings
,使用 *T
表示Set
只能在T
的可寻址值上调用。
我们现在可以这样使用
func F() {
// With the rewritten FromStrings, this is now OK.
// *Settable implements Setter.
nums := from.Strings(Settable)([]string{"1", "2"})
// Here nums is []Settable{1, 2}.
...
}
要明确的是,使用type *T Setter
并不意味着Set
方法只能是指针方法。Set
仍然可能是一种普通方法。这样就可以了,因为所有值传递方法也都在指针类型的方法集中。在此示例中,只有Set
可以将其编写为值方法才有意义,在包含指针字段的结构上定义方法时可能就是这种情况。
使用泛型类型作为匿名函数参数类型
当将实例化类型解析为匿名函数参数类型时,存在解析歧义。
var f func(x(T))
在此示例中,我们不知道该函数是否具有实例化类型的单个匿名参数x(T)
,或者这是否是(T)
类型的命名参数x
。
我们希望这表示前者:实例化类型的匿名参数x(T)
。实际上,这当前语言并不向后兼容,这意味着后者。但是,当前gofmt
程序会重写func(x(T))
为func(x T)
,因此func(x(T))
在普通的Go代码中非常不寻常。
因此,我们建议更改语言。这可能会破坏一些现有程序,但解决方法是简单地运行gofmt。这可能会改变编写没有使用gofmt的func(x(T))
的程序的含义,而是选择引入x
与带有括号类型的函数参数同名的泛型。我们认为,此类程序将极为罕见。
尽管如此,这仍然是一种风险,如果风险似乎太大,我们可以避免进行此更改。
类型参数的值不被“装箱”(boxed)
在Go的当前实现中,接口值始终包含指针。将非指针值放在接口变量中会导致该值被装箱。这意味着实际值存储在堆或堆栈上的其他位置,接口值保存指向该位置的指针。
在这种设计中,泛型的值不会装箱。例如,让我们回顾一下前面的示例from.Strings
。当用Settable
实例化时,它返回[]Settable
。例如,我们可以写
// Settable is an integer type that can be set from a string.
type Settable int
// Set sets the value of *p from a string.
func (p *Settable) Set(s string) (err error) {
// same as above
}
func F() {
// The type of nums is []Settable.
nums, err := from.Strings(Settable)([]string{"1", "2"})
if err != nil { ... }
// Settable can be converted directly to int.
// This will set first to 1.
first := int(nums[0])
...
}
当我们用Settable
类型调用from.Strings
时,我们得到一个[]Settable
(和一个错误)。该切片的元素将是Settable
值,也就是说,它们将是整数。即使它们是由泛型函数创建和设置的,也不会被装箱。
同样,当实例化泛型类型时,它将具有预期的类型作为组件。
type Pair(type F1, F2) struct {
first F1
second F2
}
实例化该字段时,这些字段将不会被装箱,并且不会发生额外的内存分配。类型Pair(int, string)
可以转换为struct { first int; second string }
。
有关类型列表的更多信息
现在让我们回顾类型列表,看一些相对的次要细节。这些不是额外规则或概念,而是类型列表工作方式的结果。
约束中的类型列表和方法
约束可以同时使用类型列表和方法。
// StringableSignedInteger is a type constraint that matches any
// type that is both 1) defined as a signed integer type;
// 2) has a String method.
type StringableSignedInteger interface {
type int, int8, int16, int32, int64
String() string
}
此约束允许其基础类型是列出的类型之一的任何类型,只要它也具有String() string
方法。值得注意的是,虽然StringableSignedInteger
约束明确列出int
,类型int
不会本身允许作为类型参数,因为int
没有一个String
方法。允许的类型参数的示例是MyInt
,定义如下:
// MyInt is a stringable int.
type MyInt int
// The String method returns a string representation of mi.
func (mi MyInt) String() string {
return fmt.Sprintf("MyInt(%d)", mi)
}
约束中的复合类型
约束中的类型可以是字面上的类型。
type byteseq interface {
type string, []byte
}
遵循通常的规则:此约束的type参数可以是string
或[]byte
或定义为这些类型之一的类型;具有此约束的泛型函数可以使用string
和允许的任何操作[]byte
。
byteseq
约束允许编写可用于string
或[]byte
类型的通用函数。
// Join concatenates the elements of its first argument to create a
// single value. sep is placed between elements in the result.
// Join works for string and []byte types.
func Join(type T byteseq)(a []T, sep T) (ret T) {
if len(a) == 0 {
// Use the result parameter as a zero value;
// see discussion of zero value in the Issues section.
return ret
}
if len(a) == 1 {
// We know that a[0] is either a string or a []byte.
// We can append either a string or a []byte to a []byte,
// producing a []byte. We can convert that []byte to
// either a []byte (a no-op conversion) or a string.
return T(append([]byte(nil), a[0]...))
}
// We can call len on sep because we can call len
// on both string and []byte.
n := len(sep) * (len(a) - 1)
for _, v := range a {
// Another case where we call len on string or []byte.
n += len(v)
}
b := make([]byte, n)
// We can call copy to a []byte with an argument of
// either string or []byte.
bp := copy(b, a[0])
for _, s := range a[1:] {
bp += copy(b[bp:], sep)
bp += copy(b[bp:], s)
}
// As above, we can convert b to either []byte or string.
return T(b)
}
类型列表中的类型参数
约束中的类型可以引用约束的类型参数。在此示例中,泛型函数Map
采用两个类型参数。要求第一类型参数具有作为第二类型参数的切片的基础类型。第二个slice参数没有限制。
// SliceConstraint is a type constraint that matches a slice of
// the type parameter.
type SliceConstraint(type T) interface {
type []T
}
// Map takes a slice of some element type and a transformation function,
// and returns a slice of the function applied to each element.
// Map returns a slice that is the same type as its slice argument,
// even if that is a defined type.
func Map(type S SliceConstraint(E), E interface{})(s S, f func(E) E) S {
r := make(S, len(s))
for i, v := range s {
r[i] = f(v)
}
return r
}
// MySlice is a simple defined type.
type MySlice []int
// DoubleMySlice takes a value of type MySlice and returns a new
// MySlice value with each element doubled in value.
func DoubleMySlice(s MySlice) MySlice {
v := Map(MySlice, int)(s, func(e int) int { return 2 * e })
// Here v has type MySlice, not type []int.
return v
}
类型转换
在具有两个类型参数的函数From
和To
,如果接受的From
约束可以被转换类型To
的约束,类型的值From
可以被转换成类型的值To
。如果任何一个类型参数不接受类型,则不允许类型转换。
通用规则的结果,通用规则可以使用类型列表中列出的所有类型允许的任何操作。
例如:
type integer interface {
type int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr
}
func Convert(type To, From integer)(from From) To {
to := To(from)
if From(to) != from {
panic("conversion out of range")
}
return to
}
Convert
允许进行类型转换,因为Go允许将每个整数类型都转换为其他整数类型。
无类型常量
某些函数可以使用无类型的常量。如果类型参数约束所接受的每种类型都允许使用无类型常量。
与类型转换一样,可以使用类型列表中列出的所有类型所允许的任何操作。
type integer interface {
type int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr
}
func Add10(type T integer)(s []T) {
for i, v := range s {
s[i] = v + 10 // OK: 10 can convert to any integer type
}
}
// This function is INVALID.
func Add1024(type T integer)(s []T) {
for i, v := range s {
s[i] = v + 1024 // INVALID: 1024 not permitted by int8/uint8
}
}
类型列表中复合类型的注释
尚不清楚我们是否完全理解类型列表中复合类型的使用。例如
type structField interface {
type struct { a int; x int },
struct { b int; x float64 },
struct { c int; x uint64 }
}
func IncrementX(type T structField)(p *T) {
v := p.x
v++
p.x = v
}
这种对IncrementX
的类型参数的约束使得每个有效的类型参数都是具有某些数字类型字段x
的结构。因此,很容易说这IncrementX
是有效的功能。这意味着的类型v
是基于类型参数的类型,隐式约束为interface { type int, float64, uint64 }
。这可能会变得相当复杂,并且这里可能有一些我们不了解的细节。
初始实现可能根本不支持类型列表中的复合类型,尽管这会使前面显示的Join
示例无效。
嵌入约束中的类型列表
当一个约束嵌入另一个约束时,最终约束的类型列表就是所涉及的所有类型列表的交集。如果存在多个嵌入式类型,则交集保留的类型参数必须满足所有嵌入式类型要求的属性。
// Addable is types that support the + operator.
type Addable interface {
type int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr,
float32, float64, complex64, complex128,
string
}
// Byteseq is a byte sequence: either string or []byte.
type Byteseq interface {
type string, []byte
}
// AddableByteseq is a byte sequence that supports +.
// This is every type is that is both Addable and Byteseq.
// In other words, just the type string.
type AddableByteseq interface {
Addable
Byteseq
}
类型列表的一般说明
在约束中显式列出类型似乎很尴尬,但是这样很清楚,在调用上允许使用哪些类型参数,以及泛型函数允许使用哪些操作。
如果语言后来更改为支持运算符方法(目前尚无此类计划),则约束将像处理其他方法一样处理它们。
预声明类型总是会有数量限制,以及这些类型支持的数量有限的运算符。将来的语言更改不会从根本上改变这些事实,因此此方法将继续有用。
这种方法不会尝试处理所有可能的运算符。尚不清楚它是否适用于复合类型。期望这些将在泛型函数和类型声明中使用复合类型来处理,而不是要求复合类型作为类型参数。例如,我们希望要索引到切片T
的函数在slice元素类型上进行参数化,并使用type的参数或变量[]T
。
如上面的示例
DoubleMySlice所示,这种方法使声明接受并返回复合类型并想要返回与其参数类型相同的结果类型的泛型函数变得笨拙。定义的复合类型并不常见,但确实会出现。这种尴尬是这种方法的缺点。
反射
我们不建议以任何方式更改反射包。当实例化类型或函数时,所有类型参数将变为普通的非泛型类型。实例化类型String
的reflect.Type
值的方法将返回在括号中带有类型参数的名称。例如,List(int)
。
非泛型代码不可能在不实例化的情况下引用泛型代码,因此,没有实例化的泛型类型或函数没有反射信息。
实践
Russ Cox 的著作指出,泛型需要在慢速的程序员,慢速的编译器或慢速的执行时间之间进行选择。
我们认为该设计允许不同的实现选择。可以为每组类型实参分别编译代码,或者可以像对每个类型实参以类似于方法调用的接口类型进行处理一样进行编译,或者可以将两者组合在一起。
换句话说,这种设计放弃了让程序员编程更慢,并绝对在慢速的编译器(分别编译每个类型的参数集)或慢速的执行时间(对类型参数的值的每个操作使用方法调用)之间进行决定。 )。
总结
尽管本文档冗长且过于细节,但实际设计仅涉及几个要点。
- 函数和类型可以具有类型参数,该类型参数是使用可选约束(接口类型)定义的。
- 约束描述了类型参数所需的方法和允许的类型。
- 约束描述了类型参数允许的方法和操作。
- 当使用类型参数调用函数时,类型推断通常会允许省略类型参数。
此设计完全向后兼容,除了一个关于func F(x(T))
的修改建议。
我们相信,这种设计可以满足人们对Go中泛型编程的需求,而不会使该语言变得不必要的复杂。
在没有多年实践经验的基础上,我们无法真正了解这些对语言的影响。这里有一些猜测。
复杂
Go的优点之一就是它的简单性。显然,这种设计使语言更加复杂。
我们认为,对于阅读良好书面通用代码而不是编写书面代码的人们来说,增加的复杂性很小。人们自然必须学习用于声明类型参数的新语法。这种新的语法以及对接口中类型列表的新支持是该设计中唯一的新语法构造。如下面的示例所示,泛型函数中的代码读取方式与普通的Go代码类似。从[]int
到[]T
是一个简单的转变。类型参数约束可以有效地用作描述类型的文档。
我们期望大多数软件包不会定义泛型类型或函数,但是许多软件包可能会使用在其他地方定义的泛型类型或函数。在通常情况下,泛型函数的工作原理与非泛型函数完全相同:你只需调用它们即可。类型推断意味着你不必显式写出类型参数。类型推断规则的设计是毫不奇怪的:正确推断出类型实参,或者调用失败并需要显式类型变量。类型推断使用类型标识,没有尝试解析两个相似但不相同的类型,这消除了相当大的复杂性。
使用泛型类型的程序包将必须传递显式类型参数。语法很熟悉。唯一的变化是将参数传递给类型,而不是仅传递给函数。
总的来说,我们试图避免设计上的一些大动作。只有时间才能证明我们的设计是否成功。
普及推广
我们希望将一些新软件包添加到标准库中。一个新的slices
包将类似于现有的byte和string包,可在任何元素类型的切片上运行。新 maps
和chans
包将提供简单的算法,这些算法当前已针对每种元素类型进行了重复实现。set
可以添加一个程序包。
新constraints
程序包将提供标准约束,例如允许所有整数类型或所有数字类型的约束。
像包container/list
和container/ring
,和类型,如sync.Map
和sync/atomic.Value
,将被更新为编译时类型安全的,或者使用新的名称或软件包的更新版本。
该math
软件包将得到扩展,为所有数值类型(例如流行Min
和Max
函数)提供一组简单的标准算法。
我们可能会在sort
包装中添加泛型变量。
可能会开发新的特殊用途的编译时类型安全的容器类型。
我们不希望像C ++ STL迭代器类型这样的方法被广泛使用。在Go中,这种想法更自然地使用接口类型表达。用C ++术语来说,将接口类型用于迭代器可能带有抽象损失,因为运行时效率将低于实际上内联所有代码的C ++方法。
随着我们获得更多的容器类型,我们可能会开发一个标准Iterator
接口。这可能反过来导致施加压力,要求修改语言以添加一些Iterator
与with range
子句一起使用的机制。不过,这是非常投机的。
效率
人们希望通过泛型代码提高哪种效率尚不清楚。
可以使用基于接口的方法来编译泛型函数而不是泛型类型。这样可以优化编译时间,因为该函数仅编译一次,但是会花费一些运行时间。
对于每个类型参数集,泛型类型可能会多次编译。显然,这将带来编译时间成本,但不会带来任何运行时间成本。编译器还可以选择使用类似于接口类型的方法来实现泛型类型,使用专用方法访问依赖于类型参数的每个元素。
只有总结经验才能知道人们对该领域的期望。
遗漏
我们认为该设计涵盖了泛型编程的基本要求。但是,还有许多不支持的构造。
- 无法编写与特定类型参数一起使用的泛型函数的多个版本。
- 没有办法编写在编译时执行代码以生成要在运行时执行的代码。
- 没有更高级别的抽象。除了调用或实例化函数外,没有其他方法可以讨论带有类型参数的函数。除了实例化泛型之外,没有其他方法可以讨论。
- 没有泛型类型的描述。为了在泛型函数中使用运算符,约束列出了特定的类型,而不是描述类型必须具有的特征。这很容易理解,但有时可能会受到限制。
- 参数不支持协变或逆变(Contravariance or Covariance)。
- 你可以编写一个编译时类型安全的通用容器,但只能使用普通方法而不是使用
c[k]
语法来访问它。 - 除了使用辅助函数或包装器类型外,没有其他方法指定一些类型参数。
- 不支持可变参数类型参数,只能通过函数接受不同数量的类型参数和常规参数。
- 没有适配器。约束无法定义可用于支持尚未实现约束的类型参数的适配器,例如,根据
==
方法定义Equal
运算符,反之亦然。 - 没有对非类型值(例如常量)进行参数化。这对于数组来说最明显,有时可能很方便编写
type Matrix(type n int) [n][n]float64
。为容器类型指定有效值有时也很有用,例如元素的默认值。
Issues
此设计存在一些问题,值得更详细的讨论。与整体设计相比,我们认为这些问题相对较小,但是仍然值得我们进行完整的思考和讨论。
零值
该设计没有简单的表达式来表示类型参数的零值。例如,考虑使用指针的可选值的以下实现:
type Optional(type T) struct {
p *T
}
func (o Optional(T)) Val() T {
if o.p != nil {
return *o.p
}
var zero T
return zero
}
在o.p == nil
的情况下,我们想要返回T
的零值,但是我们无法编写该值。可能写为return nil
会很好,但是如果T
是int
,那是行不通的。在那种情况下,我们将不得不写return 0
。而且,当然,没有办法编写约束来支持return nil
或 return 0
。
一些解决方法是:
-
var zero T
如上使用,它可以与现有设计一起使用,但需要另外声明。 - 使用
*new(T)
,这很神秘,但可以与现有设计一起使用。 - 仅针对结果,将结果参数命名为
_
,并使用裸return
语句返回零值。 - 扩展设计以允许使用
nil
任何匹配通用类型的零值(但请参见问题22729)。 - 扩展设计以允许使用
T{}
,其中T
是类型参数,以指示类型的零值。 - 如问题19642中所建议,更改语言以允许
_
在分配(包括return
或函数调用)的右侧使用。 - 如问题21182中所建议,更改语言以允许
return ...
返回结果类型的零值。
我们认为在决定确定哪种方法之前,还需要更多有关此设计的经验。
很多恼人的括号
如果无法推断出类型实参,则使用类型实参调用函数需要附加的类型实参列表。如果函数返回一个函数,并且我们调用它,则会得到更多的括号。
F(int, float64)(x, y)(s)
我们尝试了其他语法,例如使用冒号将类型参数与常规参数分开。在我们看来,当前的设计是最好的,但可能会有更好的选择。
定义复合类型
正如上面所讨论的,当一个额外的类型参数是必需的,其基础类型是复合类型,作为结果返回相同的是定义的类型。
例如,此函数将在切片上映射一个函数。
// Map applies f to each element of s, returning a new slice
// holding the results.
func Map(type T)(s []T, f func(T) T) []T {
r := make([]T, len(s))
for i, v := range s {
r[i] = f(v)
}
return r
}
但是,在调用已定义类型时,它将返回该类型的元素类型的切片,而不是已定义类型本身。
// MySlice is a defined type.
type MySlice []int
// DoubleMySlice returns a new MySlice whose elements are twice
// that of the corresponding elements of s.
func DoubleMySlice(s MySlice) MySlice {
s2 := Map(s, func(e int) int { return 2 * e })
// Here s2 is type []int, not type MySlice.
return MySlice(s2)
}
正如上面所讨论的,这可以通过使用一个额外的类型参数Map
避免,并且使用描述切片和元件类型之间的关系所需的约束条件。这种方式可行,但很尴尬。
识别匹配的预声明类型
该设计没有提供任何方法来测试与类型实参匹配的基础类型。代码可以通过转换为空接口类型并使用类型断言或类型开关的某种笨拙方法来测试实际的类型参数。这可以让代码测试实际的与基础类型不同的类型参数。
下面是一个显示差异的示例。
type Float interface {
type float32, float64
}
func NewtonSqrt(type T Float)(v T) T {
var iterations int
switch (interface{})(v).(type) {
case float32:
iterations = 4
case float64:
iterations = 5
default:
panic(fmt.Sprintf("unexpected type %T", v))
}
// Code omitted.
}
type MyFloat float32
var G = NewtonSqrt(MyFloat(64))
在初始化G
时,该代码就会抛异常,因为在NewtonSqrt
中的v
是MyFloat
,而不是float32
或float64
。该函数实际要测试的不是的类型v
,而是v
约束中匹配的类型。
处理此问题的一种方法是允许在类型T
上进行类型切换,条件是该类型T
将始终与约束中定义的类型匹配。仅当约束条件列出显式类型时才允许这种类型的切换,并且仅允许约束条件中列出的类型作为情况使用。
无法表达可转换性
该设计无法表达两个不同类型参数之间的可转换性。例如,无法编写此函数:
// Copy copies values from src to dst, converting them as they go.
// It returns the number of items copied, which is the minimum of
// the lengths of dst and src.
// This implementation is INVALID.
func Copy(type T1, T2)(dst []T1, src []T2) int {
for i, x := range src {
if i > len(dst) {
return i
}
dst[i] = T1(x) // INVALID
}
return len(src)
}
从类型T2
到类型的转换T1
是无效的,因为任何一种类型都没有允许转换的约束。更糟糕的是,通常没有办法编写这样的约束。在T1
和T2
都可能需要某种类型列表的特殊情况下,可以像前面讨论的使用类型列表进行类型转换时所述那样编写此函数。但是例如,T1
是接口类型,T2
实现了该接口的类型,这类约束无法编写。
值得注意的是,如果T1
是接口类型,则可以使用对空接口类型的转换和类型断言来编写,但这当然不是编译时类型安全的。
// Copy copies values from src to dst, converting them as they go.
// It returns the number of items copied, which is the minimum of
// the lengths of dst and src.
func Copy(type T1, T2)(dst []T1, src []T2) int {
for i, x := range src {
if i > len(dst) {
return i
}
dst[i] = (interface{})(x).(T1)
}
return len(src)
}
没有参数化方法
本设计草案不允许方法声明特定于该方法的类型参数。接收器可能具有类型参数,但是该方法不能添加任何类型参数。
在Go中,方法的主要作用之一是允许类型实现接口。尚不清楚是否有可能允许参数化方法实现接口。例如,考虑以下代码。此代码使用了多个程序包使问题更清楚。
package p1
// S is a type with a parameterized method Identity.
type S struct{}
// Identity is a simple identity method that works for any type.
func (S) Identity(type T)(v T) T { return v }
package p2
// HasIdentity is an interface that matches any type with a
// parameterized Identity method.
type HasIdentity interface {
Identity(type T)(T) T
}
package p3
import "p2"
// CheckIdentity checks the Identity method if it exists.
// Note that although this function calls a parameterized method,
// this function is not itself parameterized.
func CheckIdentity(v interface{}) {
if vi, ok := v.(p2.HasIdentity); ok {
if got := vi.Identity(int)(0); got != 0 {
panic(got)
}
}
}
package p4
import (
"p1"
"p3"
)
// CheckSIdentity passes an S value to CheckIdentity.
func CheckSIdentity() {
p3.CheckIdentity(p1.S{})
}
在此示例中,我们有一个带有参数化方法的S
类型,也有一个带有参数化方法的HasIdentity
。S
实现了HasIdentity
。因此,函数p3.CheckIdentity
可以使用int
参数调用vi.Identity
,在此示例中为参数S.Identity(int)
。但是包p3对类型p1.S
一无所知。程序中可能没有其他地方调用S.Identity
。我们需要实例化S.Identity(int)
,但是如何做到?
我们可以在链接器时期实例化它,但是在一般情况下,它要求链接器遍历程序的完整调用图以确定可能传递给CheckIdentity
的类型集。甚至在涉及类型反射的一般情况下,这种遍历也是不够的,因为反射可能会根据用户输入的字符串查找方法。因此,通常在链接器中实例化参数化方法可能需要为每个可能的类型实参实例化每个参数化方法,这是不可行的。
或者,我们可以在它的运行时间实例化。通常,这意味着使用某种JIT,或编译代码以使用某种基于反射的方法。每种方法的实施都非常复杂,并且在运行时会很慢。
或者,我们可以确定参数化的方法实际上不实现接口,但是我们为什么需要方法就不清楚了。如果我们忽略接口,则任何参数化方法都可以实现为参数化函数。
因此,尽管乍看之下参数化方法似乎很有用,但我们必须确定它们的含义以及如何实现。
废弃的想法
这种设计并不完美,随着我们积累经验,它将进一步完善。也就是说,我们已经详细考虑了许多想法。本节列出了其中的一些想法,希望这将有助于减少重复的讨论。这些想法以常见问题解答的形式列出。
协议怎么样了?
较早的泛型设计草案使用新语言构造实现了约束,称之为协议(contracts)。类型列表仅出现在协议中,而不出现在接口类型上。但是,许多人很难理解协议和接口类型之间的区别。事实证明,协议可以表示为一组相应的接口。因此,没有协议并不会减弱表达能力。我们决定简化为仅使用接口类型的方法。
为什么不使用方法而是类型列表?
类型列表很奇怪。 为什么不为所有运算符编写方法?
可以允许使用运算符作为方法名称,从而导致诸如+(T) T
的方法。不幸的是,这还不够。我们需要某种机制来描述与任何整数类型匹配的类型,以便进行不限于单个int类型的操作(例如,移位<<(integer) T
和索引编制[](integer) T
)。对于诸如之类的操作,我们需要一个无类型的布尔类型比如==(T) untyped bool
。我们需要为诸如转换之类的操作引入新的符号,或者表达一个可能在一种类型上变化的范围,这可能需要一些新的语法。我们需要某种机制来描述无类型常量的有效值。我们将不得不考虑是否支持< (T) bool
,泛型函数也可以使用它<=
,并且类似地,是否支持+(T) T
表示函数也可以使用++
。使这种方法可行也许是可行的,但并非一帆风顺。此设计中使用的方法似乎更简单,并且仅依赖于一个新的语法构造(类型列表)和一个新的名称(comparable
)。
为什么不将类型参数封装成包?
我们对此进行了广泛的调查。当你要编写一个list
程序包,并且希望该程序包包含一个Transform
函数将List
一个元素类型转换为另一个List
元素类型时,这将成为问题。包的一个实例化中的函数返回需要同一包的不同实例化的类型是非常尴尬的。
它还使包边界与类型定义混淆。没有特别的理由认为泛型类型的使用会整齐地分解为包。有时他们会,有时他们不会。
为什么不使用的语法F<T>
类似于C ++和Java?
在解析函数(例如)v := F<T>
,看到的<
是不确定的,不知道我们看到的是类型实例化还是使用<
运算符的表达式。解决这个问题需要有效的无限制的预解析。我们希望努力保持Go解析器的效率。
为什么不使用语法F[T]
?
解析类型声明时type A [T] int
,这是一个定义为泛型类型int
还是带有T
元素的数组类型,这是不确定的。但是,这可以通过type A [type T] int
泛型来解决。
解析声明func f(A[T]int)
(如类型的单个参数[T]int
)和func f(A[T], int)
(两个参数,一个类型A[T]
和一个类型int
)的解析声明表明,需要进行一些额外的预解析。这是可解决的,但增加了解析的复杂性。
该语言通常在逗号分隔的列表中允许尾随逗号,因此对于A[T,]
如果A
是泛型,则应允许使用,但对于索引表达式通常不允许。但是,解析器无法知道A
是泛型类型还是slice,array或map类型的值,因此只有在类型检查完成后才能报告此解析错误。同样,可解决但复杂。
更普遍的认知是,我们觉得方括号在页面上有点烦,括号更像Go。随着我们获得更多经验,我们未来会重新评估该决定。
为什么不使用F«T»
?
我们考虑过了,但是我们无法要求使用非ASCII。
为什么不在内置包中定义约束?
不用写出类型列表,而使用像* constraints.Arithmetic
和constraints.Comparable
*这样的名称。
列出所有可能的类型组合非常冗长。它还引入了一组新名称,不仅是代码的作者,读者也必须记住。此设计的驱动目标之一是引入尽可能少的新名称。在此设计中,我们仅引入一个新的预声明的名称。
我们希望,如果人们发现这样的名称有用,我们可以引入一个constraints
包,该包以约束的形式定义有用的名称,这些约束可以由其他类型和函数使用并嵌入其他约束中。这将在标准库中定义,同时使程序员可以灵活地在适当的地方使用其他类型的组合。
为什么不允许在类型为类型参数的值上使用类型声明?
在此设计的早期版本中,我们允许对类型为类型参数或基于类型参数的变量使用类型声明和类型判断。我们删除了此功能,因为可以将任何类型的值转换为空接口类型,然后在其上使用类型断言或类型判断。同样,有时会混淆,具有类型列表的约束中类型断言,还是类型切换将使用实际的类型实参而不是类型实参的基础类型。
与Java比较
关于Java泛型的大多数抱怨都围绕类型擦除。此设计没有类型擦除。泛型类型的反射信息将包括完整的编译时类型信息。
在Java类型中,通配符(List<? extends Number>
,List<? super Number>
)实现协变和逆变。Go缺少这些概念,这使得泛型类型更加简单。
与C ++的比较
C ++模板不对类型参数施加任何约束(除非采用了概念上的建议)。这意味着更改模板代码可能会意外破坏某处的实例。这也意味着仅在实例化时报告异常,并且可能嵌套得很深并且难以理解。这种设计通过明确的要求约束避免了这些问题。
C ++支持模板元编程,可以理解为为在编译时使用与非模板C ++完全不同的语法进行的普通编程。我们的设计没有类似功能,这样可以降低大量的复杂性。
C ++使用两阶段名称查找,其中一些名称是在模板定义的上下文中查找的,而某些名称是在模板实例化的上下文中查找的。在这种设计中,所有名称都在编写时进行查找。
实际上,所有C ++编译器都在实例化模板时编译每个模板。这会减慢编译时间。这种设计为如何处理泛型函数的编译提供了灵活性。
与Rust比较
我们的设计中描述的泛型类似于Rust中的泛型。
一个区别是,在Rust中,必须明确定义特征绑定和类型之间的关联。用Go术语,这意味着我们将不得不在某个地方声明类型是否满足约束。正如Go类型可以在没有显式声明的情况下满足Go接口一样,在本设计中,Go类型的参数可以在没有显式声明的情况下满足约束。
在这种设计使用类型列表的地方,Rust标准库为比较之类的操作定义了标准特征。这些标准特征由Rust的原始类型自动实现,也可以由用户定义的类型实现。Rust提供了相当广泛的特征列表,至少包含34个,涵盖了所有运算符。
Rust支持方法上的类型参数,而本设计不支持。
例子
以下各节是如何使用此设计的示例。这旨在解决由于Go缺乏泛型而出现的特殊场景。
Map/Reduce/Filter
这是一个如何为切片编写map, reduce, 和 filter 功能的示例。这些功能希望能够提供在Lisp,Python,Java等中的类似的功能。
// Package slices implements various slice algorithms.
package slices
// Map turns a []T1 to a []T2 using a mapping function.
// This function has two type parameters, T1 and T2.
// There are no constraints on the type parameters,
// so this works with slices of any type.
func Map(type T1, T2)(s []T1, f func(T1) T2) []T2 {
r := make([]T2, len(s))
for i, v := range s {
r[i] = f(v)
}
return r
}
// Reduce reduces a []T1 to a single value using a reduction function.
func Reduce(type T1, T2)(s []T1, initializer T2, f func(T2, T1) T2) T2 {
r := initializer
for _, v := range s {
r = f(r, v)
}
return r
}
// Filter filters values from a slice using a filter function.
// It returns a new slice with only the elements of s
// for which f returned true.
func Filter(type T)(s []T, f func(T) bool) []T {
var r []T
for _, v := range s {
if f(v) {
r = append(r, v)
}
}
return r
}
这是这些函数的一些调用示例。根据非类型参数的类型推断类型参数。
s := []int{1, 2, 3}
floats := slices.Map(s, func(i int) float64 { return float64(i) })
// Now floats is []float64{1.0, 2.0, 3.0}.
sum := slices.Reduce(s, 0, func(i, j int) int { return i + j })
// Now sum is 6.
evens := slices.Filter(s, func(i int) bool { return i%2 == 0 })
// Now evens is []int{2}.
Map keys
这是获取任何map键值的方法。
// Package maps provides general functions that work for all map types.
package maps
// Keys returns the keys of the map m in a slice.
// The keys will be returned in an unpredictable order.
// This function has two type parameters, K and V.
// Map keys must be comparable, so key has the predeclared
// constraint comparable. Map values can be any type;
// the empty interface type imposes no constraints.
func Keys(type K comparable, V interface{})(m map[K]V) []K {
r := make([]K, 0, len(m))
for k := range m {
r = append(r, k)
}
return r
}
在典型使用中,将推断出映射key和val类型。
k := maps.Keys(map[int]int{1:2, 2:4})
// Now k is either []int{1, 2} or []int{2, 1}.
Sets
许多人要求扩展或简化Go的内置map类型以支持集合类型。下面的set类型是基于类型安全的实现,尽管它使用的是方法而不是类似的运算符[]。
// Package set implements sets of any comparable type.
package set
// Set is a set of values.
type Set(type T comparable) map[T]struct{}
// Make returns a set of some element type.
func Make(type T comparable)() Set(T) {
return make(Set(T))
}
// Add adds v to the set s.
// If v is already in s this has no effect.
func (s Set(T)) Add(v T) {
s[v] = struct{}{}
}
// Delete removes v from the set s.
// If v is not in s this has no effect.
func (s Set(T)) Delete(v T) {
delete(s, v)
}
// Contains reports whether v is in s.
func (s Set(T)) Contains(v T) bool {
_, ok := s[v]
return ok
}
// Len reports the number of elements in s.
func (s Set(T)) Len() int {
return len(s)
}
// Iterate invokes f on each element of s.
// It's OK for f to call the Delete method.
func (s Set(T)) Iterate(f func(T)) {
for v := range s {
f(v)
}
}
示例:
// Create a set of ints.
// We pass (int) as a type argument.
// Then we write () because Make does not take any non-type arguments.
// We have to pass an explicit type argument to Make.
// Type inference doesn't work because the type argument
// to Make is only used for a result parameter type.
s := set.Make(int)()
// Add the value 1 to the set s.
s.Add(1)
// Check that s does not contain the value 2.
if s.Contains(2) { panic("unexpected 2") }
本示例说明如何使用此设计为现有API提供编译时类型安全的包装器。
sort
在引入sort.Slice
之前,一个普遍的抱怨是需要使用样板定义才能使用sort.Sort
。通过这种设计,我们可以如下所示添加到sort包中,:
// Ordered is a type constraint that matches all ordered types.
// (An ordered type is one that supports the < <= >= > operators.)
// In practice this type constraint would likely be defined in
// a standard library package.
type Ordered interface {
type int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr,
float32, float64,
string
}
// orderedSlice is an internal type that implements sort.Interface.
// The Less method uses the < operator. The Ordered type constraint
// ensures that T has a < operator.
type orderedSlice(type T Ordered) []T
func (s orderedSlice(T)) Len() int { return len(s) }
func (s orderedSlice(T)) Less(i, j int) bool { return s[i] < s[j] }
func (s orderedSlice(T)) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
// OrderedSlice sorts the slice s in ascending order.
// The elements of s must be ordered using the < operator.
func OrderedSlice(type T Ordered)(s []T) {
// Convert s to the type orderedSlice(T).
// As s is []T, and orderedSlice(T) is defined as []T,
// this conversion is permitted.
// orderedSlice(T) implements sort.Interface,
// so can pass the result to sort.Sort.
// The elements will be sorted using the < operator.
sort.Sort(orderedSlice(T)(s))
}
现在我们可以写:
s1 := []int32{3, 5, 2}
sort.OrderedSlice(s1)
// Now s1 is []int32{2, 3, 5}
s2 := []string{"a", "c", "b"})
sort.OrderedSlice(s2)
// Now s2 is []string{"a", "b", "c"}
同样,我们可以添加一个使用比较函数进行排序的函数,类似于sort.Slice
但该函数偏向获取值而不是切片索引。
// sliceFn is an internal type that implements sort.Interface.
// The Less method calls the cmp field.
type sliceFn(type T) struct {
s []T
cmp func(T, T) bool
}
func (s sliceFn(T)) Len() int { return len(s.s) }
func (s sliceFn(T)) Less(i, j int) bool { return s.cmp(s.s[i], s.s[j]) }
func (s sliceFn(T)) Swap(i, j int) { s.s[i], s.s[j] = s.s[j], s.s[i] }
// SliceFn sorts the slice s according to the function cmp.
func SliceFn(type T)(s []T, cmp func(T, T) bool) {
Sort(sliceFn(E){s, cmp})
}
调用此示例如下:
var s []*Person
// ...
sort.SliceFn(s, func(p1, p2 *Person) bool { return p1.Name < p2.Name })
Channels
从未编写过许多简单的泛型通道函数,因为它们必须使用反射来编写,并且调用者必须键入断言结果。通过这种设计,它们变得易于编写。
// Package chans implements various channel algorithms.
package chans
import "runtime"
// Ranger provides a convenient way to exit a goroutine sending values
// when the receiver stops reading them.
//
// Ranger returns a Sender and a Receiver. The Receiver provides a
// Next method to retrieve values. The Sender provides a Send method
// to send values and a Close method to stop sending values. The Next
// method indicates when the Sender has been closed, and the Send
// method indicates when the Receiver has been freed.
func Ranger(type T)() (*Sender(T), *Receiver(T)) {
c := make(chan T)
d := make(chan bool)
s := &Sender(T){values: c, done: d}
r := &Receiver(T){values: c, done: d}
// The finalizer on the receiver will tell the sender
// if the receiver stops listening.
runtime.SetFinalizer(r, r.finalize)
return s, r
}
// A Sender is used to send values to a Receiver.
type Sender(type T) struct {
values chan<- T
done <-chan bool
}
// Send sends a value to the receiver. It reports whether any more
// values may be sent; if it returns false the value was not sent.
func (s *Sender(T)) Send(v T) bool {
select {
case s.values <- v:
return true
case <-s.done:
// The receiver has stopped listening.
return false
}
}
// Close tells the receiver that no more values will arrive.
// After Close is called, the Sender may no longer be used.
func (s *Sender(T)) Close() {
close(s.values)
}
// A Receiver receives values from a Sender.
type Receiver(type T) struct {
values <-chan T
done chan<- bool
}
// Next returns the next value from the channel. The bool result
// reports whether the value is valid. If the value is not valid, the
// Sender has been closed and no more values will be received.
func (r *Receiver(T)) Next() (T, bool) {
v, ok := <-r.values
return v, ok
}
// finalize is a finalizer for the receiver.
// It tells the sender that the receiver has stopped listening.
func (r *Receiver(T)) finalize() {
close(r.done)
}
下一节将提供使用此功能的示例。
容器
Go中对泛型的常见要求之一是能够编写编译时类型安全的容器。这种设计使为现有容器编写编译时类型安全的包装器变得容易。我们不会为此写一个例子。这种设计还使编写不使用装箱的编译时类型安全的容器变得容易。
这是实现为二叉树的有序映射的示例。它如何工作的细节不是太重要。要点是下面两点:
- 该代码以Go风格编写,并在需要时使用键和值类型。
- 键和值直接存储在树的节点中,不使用指针,也不作为接口值装箱。
// Package orderedmap provides an ordered map, implemented as a binary tree.
package orderedmap
import "chans"
// Map is an ordered map.
type Map(type K, V) struct {
root *node(K, V)
compare func(K, K) int
}
// node is the type of a node in the binary tree.
type node(type K, V) struct {
k K
v V
left, right *node(K, V)
}
// New returns a new map.
// Since the type parameter V is only used for the result,
// type inference does not work, and calls to New must always
// pass explicit type arguments.
func New(type K, V)(compare func(K, K) int) *Map(K, V) {
return &Map(K, V){compare: compare}
}
// find looks up k in the map, and returns either a pointer
// to the node holding k, or a pointer to the location where
// such a node would go.
func (m *Map(K, V)) find(k K) **node(K, V) {
pn := &m.root
for *pn != nil {
switch cmp := m.compare(k, (*pn).k); {
case cmp < 0:
pn = &(*pn).left
case cmp > 0:
pn = &(*pn).right
default:
return pn
}
}
return pn
}
// Insert inserts a new key/value into the map.
// If the key is already present, the value is replaced.
// Reports whether this is a new key.
func (m *Map(K, V)) Insert(k K, v V) bool {
pn := m.find(k)
if *pn != nil {
(*pn).v = v
return false
}
*pn = &node(K, V){k: k, v: v}
return true
}
// Find returns the value associated with a key, or zero if not present.
// The bool result reports whether the key was found.
func (m *Map(K, V)) Find(k K) (V, bool) {
pn := m.find(k)
if *pn == nil {
var zero V // see the discussion of zero values, above
return zero, false
}
return (*pn).v, true
}
// keyValue is a pair of key and value used when iterating.
type keyValue(type K, V) struct {
k K
v V
}
// InOrder returns an iterator that does an in-order traversal of the map.
func (m *Map(K, V)) InOrder() *Iterator(K, V) {
type kv = keyValue(K, V) // convenient shorthand
sender, receiver := chans.Ranger(kv)()
var f func(*node(K, V)) bool
f = func(n *node(K, V)) bool {
if n == nil {
return true
}
// Stop sending values if sender.Send returns false,
// meaning that nothing is listening at the receiver end.
return f(n.left) &&
sender.Send(kv{n.k, n.v}) &&
f(n.right)
}
go func() {
f(m.root)
sender.Close()
}()
return &Iterator{receiver}
}
// Iterator is used to iterate over the map.
type Iterator(type K, V) struct {
r *chans.Receiver(keyValue(K, V))
}
// Next returns the next key and value pair. The bool result reports
// whether the values are valid. If the values are not valid, we have
// reached the end.
func (it *Iterator(K, V)) Next() (K, V, bool) {
kv, ok := it.r.Next()
return kv.k, kv.v, ok
}
这个包的使用效果如下:
import "container/orderedmap"
// Set m to an ordered map from string to string,
// using strings.Compare as the comparison function.
var m = orderedmap.New(string, string)(strings.Compare)
// Add adds the pair a, b to m.
func Add(a, b string) {
m.Insert(a, b)
}
append
存在预先声明的append
功能来替换样板,否则需要增加切片。在append
添加之前,bytes包中有一个Add
函数:
// Add appends the contents of t to the end of s and returns the result.
// If s has enough capacity, it is extended in place; otherwise a
// new array is allocated and returned.
func Add(s, t []byte) []byte
Add
将两个[]byte
值附加在一起,返回一个新的切片。这对于[]byte
很好,但是如果你有其他类型的切片,则必须编写基本相同的代码以附加更多值。如果那时可以使用这种设计,也许我们不会增加append
。相反,我们可以这样写:
// Package slices implements various slice algorithms.
package slices
// Append appends the contents of t to the end of s and returns the result.
// If s has enough capacity, it is extended in place; otherwise a
// new array is allocated and returned.
func Append(type T)(s []T, t ...T) []T {
lens := len(s)
tot := lens + len(t)
if tot < 0 {
panic("Append: cap out of range")
}
if tot > cap(s) {
news := make([]T, tot, tot + tot/2)
copy(news, s)
s = news
}
s = s[:tot]
copy(s[lens:], t)
return s
}
该示例使用了预先声明的copy
函数,但是没关系,我们也可以编写该函数:
// Copy copies values from t to s, stopping when either slice is
// full, returning the number of values copied.
func Copy(type T)(s, t []T) int {
i := 0
for ; i < len(s) && i < len(t); i++ {
s[i] = t[i]
}
return i
}
这些功能可以按如下使用:
s := slices.Append([]int{1, 2, 3}, 4, 5, 6)
// Now s is []int{1, 2, 3, 4, 5, 6}.
slices.Copy(s[3:], []int{7, 8, 9})
// Now s is []int{1, 2, 3, 7, 8, 9}
这段代码并未实现将string
追加或复制到[]byte
的特殊情况,因此它的效率不如预定义函数的实现那样高效。该示例仍然表明,使用此设计将允许append
和copy
使用泛型的方式编写一次,而无需任何其他特殊语言功能。
Metrics
在Go体验报告 中Sameer Ajmani描述了一种Metrics实现。每个指标都有一个值和一个或多个字段。字段具有不同的类型。定义指标需要指定字段的类型,并使用Add方法创建一个值。Add方法将字段类型作为参数,并记录该字段集的实例。C ++实现使用可变参数模板。Java实现包括类型名称中的字段数。C ++和Java实现都提供了编译时类型安全的Add方法。
下面是如何使用此设计在Go中实现编译时类型安全的类似的Add方法。因为不支持可变数量的类型参数,所以对于像Java中的不同数量的参数,我们必须使用不同的名称。此实现仅适用于可比较的类型。更复杂的实现可以接受比较函数以使用任意类型。
// Package metrics provides a general mechanism for accumulating
// metrics of different values.
package metrics
import "sync"
// Metric1 accumulates metrics of a single value.
type Metric1(type T comparable) struct {
mu sync.Mutex
m map[T]int
}
// Add adds an instance of a value.
func (m *Metric1(T)) Add(v T) {
m.mu.Lock()
defer m.mu.Unlock()
if m.m == nil {
m.m = make(map[T]int)
}
m.m[v]++
}
// key2 is an internal type used by Metric2.
type key2(type T1, T2 comparable) struct {
f1 T1
f2 T2
}
// Metric2 accumulates metrics of pairs of values.
type Metric2(type T1, T2 comparable) struct {
mu sync.Mutex
m map[key2(T1, T2)]int
}
// Add adds an instance of a value pair.
func (m *Metric2(T1, T2)) Add(v1 T1, v2 T2) {
m.mu.Lock()
defer m.mu.Unlock()
if m.m == nil {
m.m = make(map[key2(T1, T2)]int)
}
m.m[key2(T1, T2){v1, v2}]++
}
// key3 is an internal type used by Metric3.
type key3(type T1, T2, T3 comparable) struct {
f1 T1
f2 T2
f3 T3
}
// Metric3 accumulates metrics of triples of values.
type Metric3(type T1, T2, T3 comparable) struct {
mu sync.Mutex
m map[key3(T1, T2, T3)]int
}
// Add adds an instance of a value triplet.
func (m *Metric3(T1, T2, T3)) Add(v1 T1, v2 T2, v3 T3) {
m.mu.Lock()
defer m.mu.Unlock()
if m.m == nil {
m.m = make(map[key3(T1, T2, T3)]int)
}
m.m[key3(T1, T2, T3){v1, v2, v3}]++
}
// Repeat for the maximum number of permitted arguments.
使用此程序包如下所示:
import "metrics"
var m = metrics.Metric2(string, int){}
func F(s string, i int) {
m.Add(s, i) // this call is type checked at compile time
}
由于缺少对可变参数类型参数的支持,此实现具有一定的重复性。但是,使用该软件包很容易,而且类型安全。
列表转换
虽然切片是高效且易于使用的,但在某些情况下,使用链表更合适。此示例主要显示将一种类型的链表转换为另一种类型,作为使用同一泛型类型的不同实例的示例。
// Package list provides a linked list of any type.
package list
// List is a linked list.
type List(type T) struct {
head, tail *element(T)
}
// An element is an entry in a linked list.
type element(type T) struct {
next *element(T)
val T
}
// Push pushes an element to the end of the list.
func (lst *List(T)) Push(v T) {
if lst.tail == nil {
lst.head = &element(T){val: v}
lst.tail = lst.head
} else {
lst.tail.next = &element(T){val: v }
lst.tail = lst.tail.next
}
}
// Iterator ranges over a list.
type Iterator(type T) struct {
next **element(T)
}
// Range returns an Iterator starting at the head of the list.
func (lst *List(T)) Range() *Iterator(T) {
return Iterator(T){next: &lst.head}
}
// Next advances the iterator.
// It reports whether there are more elements.
func (it *Iterator(T)) Next() bool {
if *it.next == nil {
return false
}
it.next = &(*it.next).next
return true
}
// Val returns the value of the current element.
// The bool result reports whether the value is valid.
func (it *Iterator(T)) Val() (T, bool) {
if *it.next == nil {
var zero T
return zero, false
}
return (*it.next).val, true
}
// Transform runs a transform function on a list returning a new list.
func Transform(type T1, T2)(lst *List(T1), f func(T1) T2) *List(T2) {
ret := &List(T2){}
it := lst.Range()
for {
if v, ok := it.Val(); ok {
ret.Push(f(v))
}
if !it.Next() {
break
}
}
return ret
}
点积
通用点积实现,适用于任何数字类型的切片。
// Numeric is a constraint that matches any numeric type.
// It would likely be in a constraints package in the standard library.
type Numeric interface {
type int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr,
float32, float64,
complex64, complex128
}
// DotProduct returns the dot product of two slices.
// This panics if the two slices are not the same length.
func DotProduct(type T Numeric)(s1, s2 []T) T {
if len(s1) != len(s2) {
panic("DotProduct: slices of unequal length")
}
var r T
for i := range s1 {
r += s1[i] * s2[i]
}
return r
}
(注意:泛型实现方法可能会影响DotProduct
是否使用FMA,从而影响使用浮点类型时的确切结果。目前尚不清楚这是什么问题,或者是否有任何方法可以解决。)
绝对差
通过使用Abs
方法来计算两个数值之间的绝对差。这使用了上一个示例中定义的Numeric
相同约束。
此示例使用了比用于计算绝对差的简单情况更多的组件。旨在说明如何将算法的通用部分分解为使用方法的代码,其中方法的确切定义可以根据所使用的类型的种类而变化。
// NumericAbs matches numeric types with an Abs method.
type NumericAbs(type T) interface {
Numeric
Abs() T
}
// AbsDifference computes the absolute value of the difference of
// a and b, where the absolute value is determined by the Abs method.
func AbsDifference(type T NumericAbs)(a, b T) T {
d := a - b
return d.Abs()
}
我们可以定义Abs
适合于不同数字类型的方法。
// OrderedNumeric matches numeric types that support the < operator.
type OrderedNumeric interface {
type int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr,
float32, float64
}
// Complex matches the two complex types, which do not have a < operator.
type Complex interface {
type complex64, complex128
}
// OrderedAbs is a helper type that defines an Abs method for
// ordered numeric types.
type OrderedAbs(type T OrderedNumeric) T
func (a OrderedAbs(T)) Abs() OrderedAbs(T) {
if a < 0 {
return -a
}
return a
}
// ComplexAbs is a helper type that defines an Abs method for
// complex types.
type ComplexAbs(type T Complex) T
func (a ComplexAbs(T)) Abs() ComplexAbs(T) {
d := math.Hypot(float64(real(a)), float64(imag(a)))
return ComplexAbs(T)(complex(d, 0))
}
然后,我们可以通过与我们刚刚定义的类型之间进行转换来定义为调用者完成工作的函数。
// OrderedAbsDifference returns the absolute value of the difference
// between a and b, where a and b are of an ordered type.
func OrderedAbsDifference(type T OrderedNumeric)(a, b T) T {
return T(AbsDifference(OrderedAbs(T)(a), OrderedAbs(T)(b)))
}
// ComplexAbsDifference returns the absolute value of the difference
// between a and b, where a and b are of a complex type.
func ComplexAbsDifference(type T Complex)(a, b T) T {
return T(AbsDifference(ComplexAbs(T)(a), ComplexAbs(T)(b)))
}
值得注意的是,此设计的功能不足以编写如下代码:
// This function is INVALID.
func GeneralAbsDifference(type T Numeric)(a, b T) T {
switch (interface{})(a).(type) {
case int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr,
float32, float64:
return OrderedAbsDifference(a, b) // INVALID
case complex64, complex128:
return ComplexAbsDifference(a, b) // INVALID
}
}
OrderedAbsDifference
和ComplexAbsDifference
的调用是无效的,因为不是所有实现了Numeric
约束的类型可以实现OrderedNumeric
或Complex
约束。尽管类型切换意味着该代码在概念上将在运行时运行,但是不支持在编译时解析此代码。这是表达上述遗漏之一的另一种方式:此设计未提供定制化。
附录
本附录涵盖了设计的各种细节,这些细节似乎没有足够的重要性在前面的部分中进行介绍。
泛型别名
type别名可以引用泛型,但是type别名可能没有自己的参数。之所以存在此限制,是因为尚不清楚如何使用具有约束的类型参数来处理类型别名。
type VectorAlias = Vector
在这种情况下,类型别名的使用将必须提供适合于被别名化的通用类型的类型参数。
var v VectorAlias(int)
类型别名也可以指代实例化的类型。
type VectorInt = Vector(int)
实例化函数
Go通常允许在不传递任何参数的情况下引用函数,从而产生函数类型的值。你不能使用具有类型参数的函数来执行此操作;必须在编译时知道所有类型的参数。就是说,你可以通过传递类型实参来实例化该函数,但不必调用实例化。这将产生一个没有类型参数的函数值。
// PrintInts is type func([]int).
var PrintInts = Print(int)
嵌入式类型参数
当泛型类型是结构体,并且类型参数作为字段嵌入在结构体中时,该字段的名称就是类型参数的名称。
// A Lockable is a value that may be safely simultaneously accessed
// from multiple goroutines via the Get and Set methods.
type Lockable(type T) struct {
T
mu sync.Mutex
}
// Get returns the value stored in a Lockable.
func (l *Lockable(T)) Get() T {
l.mu.Lock()
defer l.mu.Unlock()
return l.T
}
// Set sets the value in a Lockable.
func (l *Lockable(T)) Set(v T) {
l.mu.Lock()
defer l.mu.Unlock()
l.T = v
}
内联约束
正如我们在interface{}
用作类型约束的示例中所看到的,约束没有必要使用命名接口类型。类型参数列表可以使用接口类型。
// Stringify calls the String method on each element of s,
// and returns the results.
func Stringify(type T interface { String() string })(s []T) (ret []string) {
for _, v := range s {
ret = append(ret, v.String())
}
return ret
}
复合类型推断
我们现在不建议使用此功能,但可以考虑该语言的未来版本。
我们也可以考虑为泛型类型复合支持类型推断。
type Pair(type T) struct { f1, f2 T }
var V = Pair{1, 2} // inferred as Pair(int){1, 2}
目前尚不清楚这在实际代码中出现的频率。
泛型函数参数的类型推断
我们现在不建议使用此功能,但可以考虑使用更高版本。
在下面的例子中,考虑在FindClose
中调用Find
。类型推断可以确定类型参数Find
是T4
,从我们知道的最后一个参数的类型必须是func(T4, T4) bool
,从我们可以推断出类型参数IsClose
也必须T4
。但是,前面介绍的类型推断算法无法做到这一点,因此我们必须显式编写IsClose(T4)
。
乍一看这似乎很深奥,在将泛型函数传递给泛型Map和Filter函数时会出现这种情况。
// Differ has a Diff method that returns how different a value is.
type Differ(type T1) interface {
Diff(T1) int
}
// IsClose returns whether a and b are close together, based on Diff.
func IsClose(type T2 Differ)(a, b T2) bool {
return a.Diff(b) < 2
}
// Find returns the index of the first element in s that matches e,
// based on the cmp function. It returns -1 if no element matches.
func Find(type T3)(s []T3, e T3, cmp func(a, b T3) bool) int {
for i, v := range s {
if cmp(v, e) {
return i
}
}
return -1
}
// FindClose returns the index of the first element in s that is
// close to e, based on IsClose.
func FindClose(type T4 Differ)(s []T4, e T4) int {
// With the current type inference algorithm we have to
// explicitly write IsClose(T4) here, although it
// is the only type argument we could possibly use.
return Find(s, e, IsClose(T4))
}
类型参数的反射
尽管我们不建议更改反射包,但将来可能要考虑的一种可能性添加两个新方法,reflect.Type:NumTypeArgument() int
将类型实参的数量返回给类型,TypeArgument(i) Type
返回第i个类型实参。NumTypeArgument
将为实例化的泛型类型返回非零值。可以为reflect.Value
定义类似的方法,对于NumTypeArgument
实例化的泛型函数,该方法将返回非零值。可能会有某种程序会关心此信息。
在类型字面量中实例化类型
在类型字面量的末尾实例化类型时,存在解析歧义。
x1 := []T(v1)
x2 := []T(v2){}
在此示例中,第一种情况是将类型v1
转换为type []T
。第二种情况是类型的复合字面量[]T(v2)
,其中T
是我们使用type参数实例化的泛型类型v2
。歧义是在我们看到开放括号的那一刻:解析器不知道它是在看到类型转换还是类似复合字面量的东西。
为了避免这种歧义,我们需要在类型文字的末尾加上类型实例化。要编写作为类型实例化切片的类型字面量,必须编写[](T(v1))
。如果没有这些括号,[]T(x)
则将解析为([]T)(x)
,而不是[](T(x))
。这仅适用于以类型名称结尾的slice,array,map,chan和func类型字面量。
嵌入实例化的接口类型
将实例化的接口类型嵌入另一个接口类型时,存在解析歧义。
type I1(type T) interface {
M(T)
}
type I2 interface {
I1(int)
}
在此示例中,我们不知道interface I2
是否具有一个名为I1
,还是要尝试将实例化类型I1(int)
嵌入到I2
中。
为了向后兼容,我们将其视为前一种情况:I2
具有名为I1
的方法。
为了嵌入实例化的接口类型,我们要求使用额外的括号。
type I2 interface {
(I1(int))
}
当前语法不允许这样做,这将放宽现有规则。
将实例化类型嵌入结构中也是如此。
type S1 struct {
T(int) // field named T of type int
}
type S2 struct {
(T(int)) // embedded field of type T(int)
}