写点什么

Go1.18 泛型浅谈

作者:CodeWithBuff
  • 2022 年 5 月 11 日
  • 本文字数:3080 字

    阅读完需:约 10 分钟

引言

刷力扣经常用到 min 和 max 函数,但是众嗦粥汁,Go 没有函数重载,所以每次需要提前写好一堆 minInt, minFloat, maxInt 的函数,真是离离原上谱!


那有没有什么方法可以实现一个方法,多种类型使用呢?别的语言有泛型来保证,Go 的话,在 18 版本之后引入了泛型,可以实现这一需求。

使用

直接看代码:


func min[T int | float64](a, b T) T {  if a < b {    return a  } else {    return b  }}
func main() { fmt.Println(min(1, 2)) fmt.Println(min(2.7, 3.1))}
复制代码


如果觉得类型约束写在方法里比较丑,还有这种方法:


type Comparable interface {  int | float64}
func min[T Comparable](a, b T) T { if a < b { return a } else { return b }}
func main() { fmt.Println(min(1, 2)) fmt.Println(min(2.7, 3.1))}
复制代码


如果懒得写常见的类型封装,可以使用官方的扩展库:


import (  "fmt"  "golang.org/x/exp/constraints")
func min[T constraints.Ordered](a, b T) T { if a < b { return a } else { return b }}
func main() { fmt.Println(min(1, 2)) fmt.Println(min(2.7, 3.1))}
复制代码


其中 Ordered 实现如下:


type Ordered interface {  Integer | Float | ~string}
// Integertype Integer interface { Signed | Unsigned}
// Floattype Float interface { ~float32 | ~float64}
// Signedtype Signed interface { ~int | ~int8 | ~int16 | ~int32 | ~int64}
// Unsignedtype Unsigned interface { ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr}
复制代码


上面我们对于泛型方法的调用,并没有指明调用参数的类型,但是却编译通过了,如果你想显式指出,可以这样:


func main() {  fmt.Println(min[int](1, 2))  fmt.Println(min[float64](2.7, 3.1))}
复制代码


如果不指名泛型的实际类型,编译期会在编译期间触发类型推断,然后特例化泛型函数,并根据推导出来的类型去调用对应的特例化方法,这点类似 C++,或者直接看下面的代码:


func main() {  iMin := min[int]  fMin := min[float64]  fmt.Println(iMin(1, 2))  fmt.Println(fMin(2.7, 3.1))}
复制代码


可以看到对于 int 和 float,编译期分别把 min 展开成了两个类型对应的方法,这是编译期间完成的,所以泛型方法调用看起来就是一个普通的函数调用,而没有运行时开销。


Providing the type argument to min, in this case int, is called instantiation. Instantiation happens in two steps. First, the compiler substitutes all type arguments for their respective type parameters throughout the generic function or type. Second, the compiler verifies that each type argument satisfies the respective constraint. We’ll get to what that means shortly, but if that second step fails, instantiation fails and the program is invalid.


对应的引用在这里。


此外,泛型亦可作用于类型,同时可以对类型特例化:


type add[T constraints.Ordered] struct {  v T}
func (a *add[T]) run(val T) T { return a.v + val}
func (a *add[T]) print() { fmt.Println(a.v)}
// 特例化addtype sAdd add[string]type iAdd add[int]
func main() { s := &add[string]{v: "Hello"} i := &add[int]{v: 1} s.print() fmt.Println(i.run(2))}
复制代码

解释

现在我们来解释一些用法。


Go 的泛型使用[]的原因在于避免<>对于大于和小于产生的歧义。此外,Go 的泛型无法直接标注,需要指出泛型类型的约束。最大级别的约束即空接口 interface{},如果你曾经学习过 Java,就知道 Java 的所有对象都继承自 Object,二者可以比较理解。


Go 对于类型约束的写法有两种,一种是直接写在[]中,另一种则是定义一个约束接口,然后通过接口指定约束集合。


比如我们期待一个类型只能是无符号数,则可以这样写:


func f[T uint8 | uint16 | uint32 | uint64 | uintptr | uint](v T) {}
复制代码


但是这样很难实现复用,所以可以把这个约束提到一个接口中,然后通过接口来约束:


type unsigned interface {  uint8 | uint16 | uint32 | uint64 | uintptr | uint}
func f[T unsigned](v T) {}
func ff[T unsigned](v1, v2 T) {}
复制代码


这也是扩展包中 Unsigned 的实现方式。但是扩展包中对于每个类型,其前面多了一个~符号。假如有T ~int,则表示 T 的约束不仅仅是 int,所有底层类型为 int 的自定义类型亦可满足 T 的约束:


type int0 int
func add[T ~int](v1, v2 T) T { return v1 + v2}
func main() { var v1, v2 int0 v1 = 1 v2 = 2 fmt.Println(add(v1, v2))}
复制代码

接口:旧瓶装新酒?

这里来解释为什么接口可以做到类型约束。


首先,接口是怎么定义的?接口的定义为方法的集合。任何实现了接口所有方法的类型,都称为实现了该接口:


如上图所示,类型 P,Q,R... ...都实现了接口。但是从另一种层面来说,接口也定义了一个类型集合,集合中的元素都实现了这一接口。


此时接口的语义转变成了类型的集合。这是我们如何理解约束条件通过接口组织的关键。

既然接口可以理解成类型的集合,那我们为何不直接在接口里放类型呢?


这样一来就完成通过接口对类型的约束了。当然,类型集合的接口也可以存在方法。此外,如果接口约束很简单的话,也可以使用单行写法:


func f[T ~[]E, E interface{}]() {}
复制代码

约束推断

现在我们来看一个场景:


func more[T ~[]E, E constraints.Integer](slice T, factor E) []E {  tmp := make([]E, len(slice))  for i, e := range slice {    tmp[i] = e * factor  }  return tmp}
func main() { slice := []int{1, 2, 3} tmp := more(slice, 2) for _, e := range tmp { fmt.Printf("%d ", e) }}
复制代码


这是一段简单的对切片元素扩大 N 倍的代码。现在我们有一个自定义类型:


func more[T ~[]E, E constraints.Integer](slice T, factor E) []E {  tmp := make([]E, len(slice))  for i, e := range slice {    tmp[i] = e * factor  }  return tmp}
type myInts []int
func (m myInts) print() { for _, e := range m { fmt.Printf("%d ", e) }}
func main() { slice := myInts{1, 2, 3} tmp := more(slice, 2) tmp.print() // 这里会编译失败}
复制代码


可是这是为什么呢?接着看:


func more[T ~[]E, E constraints.Integer](slice T, factor E) T { // 仅仅改了返回值  tmp := make([]E, len(slice))  for i, e := range slice {    tmp[i] = e * factor  }  return tmp}
type myInts []int
func (m myInts) print() { for _, e := range m { fmt.Printf("%d ", e) }}
func main() { slice := myInts{1, 2, 3} tmp := more(slice, 2) tmp.print() // OK}
复制代码


在这里,我们仅仅更改了泛型函数的返回值类型为切片的类型,而非切片元素类型的切片([]E -> T),所以保留了 T 的完整信息,包括它的方法,此时方法调用成功。


当然,我们强转一下也是可以的:


func main() {  slice := myInts{1, 2, 3}  tmp := myInts(more(slice, 2))  tmp.print() // OK}
复制代码


现在来思考一个问题,为什么类型推断成功了?明明我们没有指定 factor 的类型,而 Integer 有 int, int32, int64 等多种类型,单靠输入的数字'2'是无法确定的不是吗?


答案是 factor 和 slice 的元素是同一类型,而我们可以通过推断 slice 的类型确定元素类型,进而确定 factor 的类型,这就是约束推断。


上述只是一个引言。Go1.18 的约束推断比较复杂,这里留坑,并放入链接。

引用

An Introduction To Generics


Type Parameters Proposal

发布于: 刚刚阅读数: 3
用户头像

CodeWithBuff

关注

Wubba lubba dub dub. 2021.03.13 加入

上学中...

评论

发布
暂无评论
Go1.18泛型浅谈_golang_CodeWithBuff_InfoQ写作社区