Golang 泛型相关笔记

Posted on 2023年7月22日周六 技术

类型限制

范型为了使类型限制的表达更加宽泛,把 interface 的定义从“方法的集合”扩展到了“类型的集合”,两者主要的区别是内置运算符的不同。在 Go 中, < , == 等内置运算符是不支持重置的,且只有内置类型可以拥有这些运算符,自定义类型是无法拥有和定义这类运算符的。原先 interface 作为“方法的集合”,无法表达出一个类型是否支持比较相等等内置运算(因为内置运算不能被定义)。为了解决这个问题, Go 引入了 underlying type 并规定凡是 underlying type 支持内置运算符的类型都能复用其 underlying type 的内置运算。interface 也不仅仅可以用来表达方法集合,而是可以用 ~underlying type 等语法表示其对应类型集合。

Go 的泛型由于类型限制的存在,看上去会和 interface 类似,在这里与 interface 的主要不同是相关方法的返回值可以是具体的类型,而不只是 interface 。同时,还有装箱(box)的区别,一个interface里包含了两个值,代表其指向元素的type和value,如果用 interface 就会发生装箱,而泛型不会有此问题。

类型推断

技术上类型推断主要由三个子技术组成:

类型归一化

名词定义

  1. 一致:identical,指两个类型字面意义上的完全一样,例如 int 和 int 是 identical 的,type MyInt int 和 int 就不是了。
  2. 等价:equivalent,比 identical 更加宽泛,两个类型如果可以互相转化,那么就可以被判定为等价,例如上面的 type MyInt int 和 int 虽然不一致,但却是等价的。

功能描述

  1. 给出两个类型,如果两个类型都没有类型参数,给出这两个类型是否相等。
  2. 如果两个类型中有类型参数,给出这两个类型是否可能相等。

具体操作

  1. 带有类型参数的类型,在不考虑类型参数的前提下,该类型与比较类型在结构上必须一致 ( identical ),否则归一失败。
    1. 例如 []map[T1]T2[]T3 在结构上是一致的, T3 可以被替换成 map[T1]T2,同理 []map[T1]bool[]map[string]T2 在结构上也是一致的。
    2. 例如 []map[T1]T2int, struct{}, []struct{} 等类型在结构上不可能一致。
  2. 不带有类型参数的类型,与比较类型必须等价 ( equivalent ),否则归一失败。
    1. 两个类型一致自然等价。
    2. 如果两个类型是 channel 类型,忽略 channel 方向后类型一致,也可以判定为等价。
    3. 如果两个类型的 underlying types 是一致的,那也可以判定为等价。

函数实参类型推断

名词定义

  1. 参数:parameter,指函数定义时指定需要传入的参数,实际上是一个占位符,没有真实值,例如 func Add(a, b int) int 中的 a 和 b 就是参数。
  2. 实参:argument,指函数被调用时实际传入的参数,是一个真实值,例如 sum := Add(a, 2) 中的 a 和 2 就是真实值,此示例中 a 是一个命名变量或常量,2 是一个未命名常量。

功能描述

  1. 在调用有类型参数的函数时,若调用方没有传入类型参数,则根据实参推断出类型参数。

具体实现

  1. 只有参数列表中使用的类型参数才能被推断,如果一个类型参数只作为函数的返回值或者只在函数体中被使用,那么这个类型参数无法被推断。
  2. 忽略被传入的类型参数(如果有的话,一部分类型推断就无需执行了),对传入的实参和参数进行归一化,归一化失败则类型推断失败。
  3. 对实参和参数进行归一化的过程分为两步,第一步先忽略传入的常量,因为常量的类型是不确定的,如果没有常量,或者忽略常量后也推断出了相关的类型参数,那么完成类型推断。
  4. 如果上述过程也没有完成类型推断,认定常量的类型为 default type,继续类型推断。

限制类型推断

功能描述

  1. 根据定义的类型参数限制,从一个已知类型参数推导出其他暂时未知的类型参数。
  2. 例如有一个函数 func Double[S ~[]E, E constraints.Integer] (s S) S ,这个函数被这样调用 Double([]int{1, 2, 3}),可以从 S -> []int, S -> ~[]E 推断出,`E -> int`。
  3. https://go.googlesource.com/proposal/+/HEAD/design/43651-type-parameters.md#pointer-method-example 展示了对泛型方法传入非指针类型,结果无法调用类型上定义的指针方法,并利用限制类型推断解决了该问题。

具体实现

  1. 当一个类型参数对应的类型限制对应的 type set 只有一个类型或一个 underlying type 时,可以执行限制类型推断。
  2. 这一段为本人理解:在类型推断中会维护一个类型参数到类型的 map,这个 map 允许 key 即类型参数是重复的(类似 multimap),一旦一个类型参数对应到了多个类型,就会对这些类型进行归一化,得到新的类型参数到类型的映射关系,并消除重复 entry。
  3. 执行完所有的 constraint type inference 后,检查实际的类型是否满足类型限制规定的方法。

实际进行类型推断的执行步骤

  1. 利用显式传入的类型参数,构建出类型参数到类型的 mapping 关系。
  2. 执行第一次限制类型推断。
  3. 利用 typed arguments 进行函数实参类型推断。
  4. 再进行一次限制类型推断。
  5. 对剩下的 untyped arguments 进行函数实参类型推断。
  6. 最后进行一次限制类型推断。

可以看到限制类型推断其实就是用来整理 mapping 关系,把类型参数相同的映射关系进行归一化。

其他

一个泛型的类型可以指向其自身,但是类型参数的值和顺序不能被改变,见下:

// This type is INVALID.
type P[T1, T2 any] struct {
	F *P[T2, T1] // INVALID; must be [T1, T2]
}
// ListHead is the head of a linked list.
type ListHead[T any] struct {
	head *ListElement[T]
}

// ListElement is an element in a linked list with a head.
// Each element points back to the head.
type ListElement[T any] 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]
}

https://research.swtch.com/generic 里面说了一个泛型窘境,编码速度、编译速度、运行速度在遇到泛型时不能被同时满足。

泛型不支持 method,主要的原因是 method 是用来和 interface 互相配合的,而 go 的 interface 不需要继承之类的绑定关系,直接转化就好了;这时候再允许 interface 中有 method 拥有自己独立的类型参数,就需要考虑何时确定类型参数的问题,对编译器的负担较大。https://go.googlesource.com/proposal/+/HEAD/design/43651-type-parameters.md#No-parameterized-methods 列举了一些方案和为什么不采用这些方案的理由。

Underlying type
  1. 自身就是underlying type的类型
    • boolean
    • numeric
    • string
    • type literal
      • ArrayType
      • StructType
      • PointerType
      • FunctionType
      • InterfaceType
      • SliceType
      • MapType
      • ChannelType
  2. 不符合以上的情况时,一个类型的underlying type是其被创建时所指向的类型的underlying type。比如:
    type (
        A1 = string
        A2 = A1
    )

    这里A1符合规则1,其 underlying type 是 string,A2符合规则2,其 underlying type 是 string

  3. 类型参数的underlying type是其类型限制的underlying type。如:

    func f[P any](x P) { ... }

    P类型参数的underlying type是any的underlying type: interface()

Constant default type

常量在编译时决定类型,除下面类型外,其他类型不能是常量

  • numbers
  • characters (runes)
  • strings
  • booleans

The default type of an untyped constant is bool, rune, int, float64, complex128 or string respectively