泛型介绍

Functions types

泛型为语言添加了三个重要的东西:

  • 1 函数和类型的类型参数。
  • 2 将接口类型定义为类型集,包括没有方法的类型。
  • 3 类型推断,它允许在调用函数时在许多情况下省略类型参数。
1、类型参数(Type Parameters)
函数和类型现在允许有类型参数不同之处在于它使用方括号而不是圆括号
Min
func Min(x, y float64) float64 {if x < y {return x}return y
}
Tfloat64T
import "golang.org/x/exp/constraints"func GMin[T constraints.Ordered](x, y T) T {if x < y {return x}return y
}

现在可以用类型参数调用这个函数,方法是编写如下的调用

x := GMin[int](2, 3)
GMinint

成功实例化之后,我们就有了一个非泛型函数,可以像调用其他函数一样调用它。例如,在类似的代码中

fmin := GMin[float64]
m := fmin(2.71, 3.14)
GMin[float64]Min

类型参数也可以与类型一起使用。

type Tree[T interface{}] struct {left, right *Tree[T]value       T
}func (t *Tree[T]) Lookup(x T) *Tree[T] { ... }var stringTree Tree[string]
Tree TLookupTree[string]stringTree
2、类型集合(Type sets)

让我们更深入地了解可用于实例化类型参数的类型实参。

float64Minfloat64

类似地,类型参数列表对每个类型参数都有一个类型。因为类型形参本身就是一个类型,所以类型形参的类型定义了类型集。这种元类型称为类型约束(type constraint)。

GMinOrdered<<=>GMinGMin
constraints.Ordered<

为了做到这一点,我们以一种新的方式来看待接口。

直到最近,Go规范还说接口定义了一个方法集,它大致是接口中枚举的方法集。任何实现了所有这些方法的类型都实现了那个接口。

在这里插入图片描述
但是另一种看待这个的方式是说接口定义了一组类型,也就是实现那些方法的类型。从这个角度来看,作为接口类型集元素的任何类型都实现了接口。

在这里插入图片描述
这两个视图导致相同的结果:对于每一组方法,我们可以想象实现这些方法的相应类型集,这就是被接口定义的类型集。

但是,就我们的目的而言,类型集视图比方法集视图有一个优势:我们可以显式地向集合添加类型,从而以新的方式控制类型集。

interface{ int|string|bool }intstringbool
intstringbool constraints.Ordered
type Ordered interface {Integer|Float|~string
}
OrderedIntegerFloatconstraintsOrdered
string~~stringstringstringtype MyString string

当然,我们仍然希望在接口中指定方法,并且希望向后兼容。在Go 1.18中,接口可以像以前一样包含方法和嵌入接口,但它也可以嵌入非接口类型、联合和底层类型集。

CPC
Ordered
[S interface{~[]E}, E interface{}]
S
interface{}
[S ~[]E, E interface{}]
any
[S ~[]E, E any]

接口作为类型集是一种强大的新机制,也是在Go语言中实现类型约束的关键。目前,使用新语法形式的接口只能用作约束。但是,不难想象显式类型约束接口在一般情况下是多么有用。

3、类型推理(Type inference)

最后一个新的主要语言特性是类型推断。在某些方面,这是对语言最复杂的改变,但它很重要,因为它让人们在编写调用泛型函数的代码时使用自然的风格。

3.1 函数实参类型推断

GMin
func GMin[T constraints.Ordered](x, y T) T { ... }
Txy
var a, b, m float64m = GMin[float64](a, b) // explicit type argument
T
var a, b, m float64m = GMin(a, b) // no type argument
abxy
function argument type inference
 MakeT[T any]() TT

3.2 约束类型推断

constraint type inference
// Scale returns a copy of s with each element multiplied by c.
// This implementation has a problem, as we will see.
func Scale[E constraints.Integer](s []E, c E) []E {r := make([]E, len(s))for i, v := range s {r[i] = v * c}return r
}

这是一个泛型函数,适用于任何整数类型的切片。

PointPoint
type Point []int32func (p Point) String() string {// Details not important.
}
PointPointScale
// ScaleAndPrint doubles a Point and prints it.
func ScaleAndPrint(p Point) {r := Scale(p, 2)fmt.Println(r.String()) // DOES NOT COMPILE
}
r.String undefined (type []int32 has no field or method String)
Scale[]EEPoint[]int32Scale[]int32Point
Scale
// Scale returns a copy of s with each element multiplied by c.
func Scale[S ~[]E, E constraints.Integer](s S, c E) S {r := make(S, len(s))for i, v := range s {r[i] = v * c}return r
}
SS[]ESEmakeS[]E
PointPointScaleScaleAndPrint
ScaleScale(p, 2)Scale[Point, int32](p, 2)ScaleSEScaleSPointEintE
约束类型推断
~typeScaleS~[]E[]ESESE

这只是对约束类型推断的介绍。有关详细信息,请参阅提案文档或语言规范。

3.3 实践中的类型推理

类型推断如何工作的确切细节是复杂的,但使用它并不复杂:类型推断要么成功,要么失败。如果成功,则可以省略类型参数,并且调用泛型函数看起来与调用普通函数没有什么不同。如果类型推断失败,编译器将给出错误消息,在这种情况下,我们可以只提供必要的类型参数。

在向语言中添加类型推理时,我们试图在推理能力和复杂性之间取得平衡。我们希望确保当编译器推断类型时,这些类型不会令人惊讶。我们尽量小心地避免在未能推断出类型的情况下出错,而不是推断出错误类型。我们可能还没有完全正确,我们可能会在未来的版本中继续完善它。其结果是,可以编写更多不需要显式类型参数的程序。今天不需要类型参数的程序明天也不需要它们。

4、结尾

泛型是1.18中一个重要的新语言特性。这些新的语言变化需要大量的新代码,而这些代码还没有在生产环境中进行过重要的测试。只有当越来越多的人编写和使用泛型代码时,这种情况才会发生。我们相信这个功能实现得很好,质量也很高。然而,与Go的大多数方面不同,我们无法用现实世界的经验来支持这种信念。因此,虽然我们鼓励在有意义的地方使用泛型,但在生产环境中部署泛型代码时,请谨慎使用。

撇开这些谨慎不谈,我们很高兴有了可用的泛型,我们希望它们能让Go程序员更有效率。

22 March 2022