4. 约束和类型集
在本章中,约束和类型集的相关内容是交叉进行的。原因是约束产生了类型集,但是类型集又反作用于约束,制约了满足约束的类型的操作范围。
建议先看下之前的内容:
Go 泛型(1)泛型和非泛型代码
crazstom,公众号:crazstomGo 泛型(1)泛型和非泛型代码
Go 泛型(2)泛型函数和泛型类型
crazstom,公众号:crazstomGo 泛型(2)泛型函数和泛型类型
4.1. 约束定义
4.1.1. 方法签名
Go 已经有一个结构可以满足约束的定义:interface 类型。一个 interface 类型是一个方法集合。
一个类型可以转换为 interface 的前提是这个类型实现了该 interface 中的所有方法。
在非泛型函数中,把一个类型作为实参赋值给 interface 形参的前提是:这个实参必须实现 interface 中的所有方法。而泛型函数中,传入的类型实参必须满足类型形参的约束条件。写一个泛型函数就像使用 interface 类型的值:泛型代码只能使用约束条件允许的操作。
因此,在本设计中,约束就是简化的 interface 类型。满足一个约束的条件意味着实现了这个 interface 类型。
假设我们需要的泛型函数需要一个约束,它的类型参数必须有一个 String() 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 中的是一个 interface,而上面的定义是一个约束。
这个约束的使用也是很简单的:
// Stringify calls the String method on each element of s,
// and returns the results.
func Stringify[T Stringer](s []T) (ret []string) {
for _, v := range s {
ret = append(ret, v.String())
}
return ret
}
在类型参数后面跟上它的约束条件,上面代码中,就是定义的 Stringer 约束。在 Stringify 函数中,这个类型 T 只可以使用约束中定义的 String 方法。是不是和 interface 的概念很像。
4.1.2. 类型元素
接下来,约束就不像 interface 了。
约束中还可以添加约束元素。约束元素可以在一行中声明,它们的类型集取并集;也可以在多行中声明,它们的类型集取交集。在一行中声明的多个约束元素合称为 union 元素,在上面的 Stringer 约束中添加:
type Stringer interface {
type int, float64
String() string
}
这样满足这个约束的类型必须有这两个特征:
-
底层类型是 int 或者 float64 类型 -
具有 String() string 方法
如:
type MyInt int
func (mi MyInt) String() string {
return fmt.Sprintf("%d--", mi)
}
4.1.3. 内嵌约束
接口中可以内嵌接口,那约束中可以内嵌另一个约束吗?答案是可以的。
Stringer 也可以写成:
type Numberic interface {
type int, float64
}
type Stringer interface {
Numberic
String() string
}
那 Stringer 的类型集就是既满足 Numberic 约束,又有 String() string 方法的所有类型。
4.1.4. any 类型
满足 Stringer 约束的类型只有 string() string 方法,那之前使用的 any 约束呢?
前面我们知道,任何类型都满足 any 约束,即 any 约束的类型集是所有类型的集合。
一个泛型函数使用 any 作为类型参数的约束,那么函数体中可以对满足 any 约束的类型 T 进行的操作有:
-
声明 T 类型的变量 -
把 T 类型的值赋给变量 -
把 T 类型的变量传入函数或者从函数中返回 -
取变量地址 -
把这些变量转换成 interface{}或者赋值给 interface{}的变量 -
把 T 类型的值转换成 T 类型(可以但没必要) -
使用类型断言把 interface{}转换成 T 类型的值 -
使用 T 类型作为 switch 的一个 case -
定义并使用 T 类型的复合类型,例如这个类型的切片 -
把 T 类型传给某些预定义的函数,例如 new
满足 any 约束的类型集其实就是一个全集,我们之后定义的约束都包含于该类型集。
4.2. 类型集
终于讲到类型集了哈哈。
4.2.1. 当前类型的类型集
其实满足上面 Stringer 约束的所有类型的集合就称为类型集。对我们来说可能是一个新名词,但是只要学过高中的集合,就会很容易理解这个概念。
首先分析 Go 现有类型的类型集。
每个类型都有一个相关的类型集合。非接口类型 T 的类型集合就是集合{T}:一个集合只有 T 它自己。任意接口类型的类型集合中的元素是实现了接口中所有方法的所有类型。
现在,任意接口类型的类型集合是一个无穷大的集合。对于任何给定的类型 T 和接口类型 IT,很容易就区分出来 T 是不是在 IT 的类型集合中(通过检查 IT 的所有方法是不是 T 都实现了),但是没有有效的方法来枚举 IT 类型集合中所有的类型。IT 也是它的类型集合中的一员,因为一个接口也声明了它自己的方法。空接口 interface{}的类型集合就是所有可能类型的集合。
通过接口中的元素来构造接口类型的类型集合是很方便的。这会在不同的方式下都可以产生相同的结果。一个接口中的元素可能是方法签名或者内嵌的接口类型。虽然一个方法签名并不是类型,它也可以产生一个类型集合:所有声明该方法的类型集合。内嵌接口类型 E 的类型集合就是所有声明 E 中方法的类型。
对于任何的类型签名 M,interface{M}的类型集合就是 M:所有声明了 M 的类型。对于任何的签名 M1 和 M2,interface{M1, M2} 的类型集合就是同时声明了 M1 和 M2 的所有类型的集合。它是 M1 的类型集合与 M2 的类型集合的交集。M1 的类型集就是所有声明了 M1 的类型,M2 也是这样。如果把这两个类型集合取交集,结果就是所有同时声明了 M1 和 M2 的类型集合。其实也就是 interface{M1, M2} 的类型集。
内嵌类型也是同理。对于两个接口类型 E1 和 E2,interface{E1, E2}的类型集合就是 E1 和 E2 的类型集合取交集。
因此,一个接口类型的类型集就是接口中所有元素类型集的交集。
4.2.2. 约束的类型集
好的回到约束的类型集中,这是 Stringer 约束的定义:
type Stringer interface {
type int, float64
String() string
}
一条一条的看:
type int, float64:这句声明是一个 union 元素,其中有两个元素,当然也可以是三个、四个…。其中,int 的类型集包括所有底层类型是 int 的类型;float64 包括所有底层类型是 float64 的类型。这句声明中,需要把这些类型集取并集,得到的集合就是x
String() string:这句声明就是一个方法签名,它的类型集就是所有实现了该方法的类型。
那这个约束的类型集呢?就是每一条声明的类型集合取交集:具有 String() string 方法,底层类型是 int 或者 float64 的所有类型。
4.3. 操作符
4.3.1. Numberic 类型比较
代码中,返回切片中最小的元素,这个切片是非空的。
// This function is INVALID.
func Smallest[T any](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 的约束是 any。约束是 any 的前提下,Smallest 只能使用any 类型集中类型都支持的操作符,而不是所有支持 < 的 Go 类型。
那你可能会说了,定义一个约束,使用类型元素不就可以了?是可以的。
对于前面显示的 Smallest 示例,我们可以使用这样的约束:
// 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
}
根据该约束,Smallest 可以改写成:
// Smallest returns the smallest element in a slice.
// It panics if the slice is empty.
func Smallest[T Ordered](s []T) T {
r := s[0] // panics if slice is empty
for _, v := range s[1:] {
if v < r {
r = v
}
}
return r
}
4.3.2. 其他类型的等值比较
在 Go 中,除了 int、float64 这些数字类型可以比较,还有其它可以比较的类型:结构体可以使用 == 和 != 进行比较。
那为了在泛型函数中支持该比较方式, Go 内置了一个 comparable 约束,comparable约束的类型集是所有可比类型的集合。这允许在该类型的值上使用==和!=。
例如,可以使用任何可比类型实例化此函数:
// Index returns the index of x in s, or -1 if not found.
func Index[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 以产生任何类型无法满足的约束。另请参阅下面的空类型集的讨论。
// ImpossibleConstraint is a type constraint that no type can satisfy,
// because slice types are not comparable.
type ImpossibleConstraint interface {
comparable
[]int
}
4.4. 互相引用的类型参数
这一节是比较复杂的,请大家细心看。
在同一个类型参数列表中,约束可以引用其他类型参数。
例如,一个泛型 graph 包,里面包含了操作图像的泛型算法。这个算法使用两种类型,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[Edge any] 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[Node any] interface {
Nodes() (from, to Node)
}
// Graph is a graph composed of nodes and edges.
type Graph[Node NodeConstraint[Edge], Edge EdgeConstraint[Node]] struct { ... }
// New returns a new graph given a list of nodes.
func New[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 { ... }
上面的约束定义是一个高级用法,不过也是很简单的。约束是一种 Go 的类型,那之前说到类型使用类型参数成为泛型类型,那约束也可以使用类型参数,成为泛型约束。
在 New 函数的类型参数列表中,Node 具有一个 NodeConstraint[Edge] 约束,这个约束就引用后面的 Edge;而后面的类型参数 Edge 具有一个 EdgeConstraint[Node] 约束;两个类型参数的约束中进行相互引用,这在 Go 泛型中是可行的。
在 Graph 函数中 Node 的约束中,传递给类型约束 NodeConstraint 的 Edge 是 Graph 的第二个类型参数。这将使用类型参数 Edge 实例化 NodeConstraint。我们看到 Node 必须有一个方法 Edges,它返回 []Edge,这正是我们想要的。而满足 EdgeConstraint 的类型必须有一个 Nodes 方法。值得注意的是,虽然乍一看这可能是接口类型的典型用法,但 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 没有实现约束条件。Edges 和 Nodes 方法定义在指针类型 *Vertex 和 *FromTo 上;Vertex 和 FromTo 类型没有任何方法。
当使用泛型约束时,首先使用类型参数列表中提供的类型参数实例化该约束,然后将相应的类型参数与实例化的约束进行比较。在这个例子中,graph.New 的 Node 类型参数有一个约束 NodeConstraint[Edge]。当我们使用*Vertex 作为 Node 类型参数和*FromTo 作为 Edge 类型参数调用graph.New 时,为了检查Node 上的约束,编译器使用类型参数*FromTo 实例化NodeConstraint。这会产生一个实例化的约束,在这种情况下,要求 Node 有一个方法 Edges() []*FromTo,并且编译器验证 *Vertex 满足该约束。
尽管 Node 和 Edge 不必使用接口类型进行实例化,但也可以使用接口类型。
type NodeInterface interface { Edges() []EdgeInterface }
type EdgeInterface interface { Nodes() (NodeInterface, NodeInterface) }
我们可以使用 NodeInterface 和 EdgeInterface 类型实例化 graph.Graph,因为它们实现了类型约束。没有太多理由以这种方式实例化类型,但这是允许的。
这种类型参数引用其他类型参数的能力说明了一个重要点:任何尝试向 Go 添加泛型都应该要求可以实例化具有多个类型参数的泛型代码,同时这些类型参数可以相互引用。
4.5. 类型推断
大多数情况下,我们可以使用类型推断来避免写类型实参。我们可以从函数的非类型实参中推导出类型实参,也可以使用约束类型推断从已知的类型实参推导出未知的类型实参。
在上面的示例中,当实例化泛型函数或类型时,我们始终为所有类型参数指定类型参数,但是在调用函数或者实例化类型的时候可以省略部分或所有类型实参,但是如果省略了部分类型实参,那么传入的类型实参必须是参数列表中的第一个类型参数。
例如,一个函数:
func Map[F, T any](s []F, f func(F) T) []T { ... }
可以以这些方式调用。(我们将在下面解释类型的推理如何详细运行;此示例是展示如何处理不完整的类型参数列表。)
var s []int
f := func(i int) int64 { return int64(i) }
var r []int64
// Specify both type arguments explicitly.
r = Map[int, int64](s, f)
// Specify just the first type argument, for F,
// and let T be inferred.
r = Map[int](s, f)
// Don't specify any
r = Map(s, f)
如果在不指定所有类型参数的情况下使用通用函数或类型,且无法推断出任何未指定的类型参数时,则会报错。
4.5.1. 类型统一
类型推断基于类型统一(算法?理论?)。
什么是类型统一呢?
现在定义一个泛型函数:
func MyFunc[T1 any, T2 any] (input map[T1]T2) {}
它的类型参数有 T1、T2,常规参数是 map[T1]T2 类型;调用者在传入常规实参的时候,Go 泛型会进行类型统一判断。
例如传入一个 map[string]bool作为 input 的实参,那么 string 就是 T1、bool 就是 T2,称为 map[T1]T2 类型统一于 map[string]bool;而如果传入一个[]int,那就会编译错误,类型统一失败。
总的来说,类型统一用于判断常规形参和常规实参是否匹配。
例如,如果T1和T2是类型参数,则可以使用以下任何一种常规形参的类型统一常规实参[]map[int] bool:
-
[]map[int]bool -
T1 (T1 匹配 []map[int]bool) -
[]T1
(T1
匹配map[int]bool
) -
[]map[T1]T2
(T1
匹配int
,T2
匹配bool
)
(这不是一个唯一列表,还有其他可能的类型统一。)
另一方面,[]map[int] bool无法与下面任何一个例子统一
-
int
-
struct{}
-
[]struct{}
-
[]map[T1]string
(此列表当然也不是唯一的)
4.5.2. 函数实参类型推断
函数参数类型推断在调用泛型函数时使用,原理是通过常规实参推断类型实参。实例化类型时不使用函数参数类型推断,实例化但未调用函数时也不使用函数参数类型推断。
为了看看它是如何工作的,让我们回到调用简单 Print 函数的例子:
func Print[T any] (data []T) {
fmt.Println("%+v", data)
}
Print[int]([]int{1, 2, 3})
这个函数调用中的类型实参 int 可以从常规实参的类型推断出来。
可以推断的类型参数是那些用于函数(非类型)输入参数的类型参数。如果有一些类型参数只用于函数的结果参数类型,或者只在函数体中使用,那么这些类型参数不能使用函数实参类型推断获得。
为了推断函数类型参数,我们将函数调用时传入参数的类型与函数的常规类型参数的类型统一起来。在调用方,我们有常规实参的类型列表,对于 Print 示例,它是 []int。在函数侧是函数的常规形参的类型列表,对于 Print 来说是 []T。在列表中,我们不管函数定义中不使用类型参数的常规参数参数。然后将类型统一应用于剩余的参数类型。
函数参数类型推断是一种two-pass算法。在第一遍pass中,我们忽略调用侧传入的常规实参中的无类型常量及其在函数定义中的相应类型。我们使用两次传递,以便在某些情况下后面的参数可以确定无类型常量的类型。
我们对列表中的相应类型进行类型统一。这将使我们将函数端的常规形参类型与调用方的常规实参类型关联起来。
第一遍pass之后,我们检查调用方输入的所有无类型常量。如果没有无类型常量,或者函数定义的类型参数已经匹配其他输入实参的类型,则类型统一完成。
否则,第二遍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[F, T any](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,用int匹配 F。使用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[F any](f1, f2 F) *Pair[F] { ... }
在调用 NewPair(1,2)时,两个参数都是没有类型的常量,所以两者都在第一次pass中忽略。在第一次pass后,我们仍有两个没有类型的常数。两者都设置为默认类型,int。第二次运行pass 中使用int,因此最终调用是NewPair[int](1,2)。
在调用 NewPair(1,int64(2)) 中,第一个参数是一个没有类型的常量,所以我们在第一次通过中忽略它。然后,我们使用F统一int64,因此最终调用是Newpair [int64](1,int64(2))。
在调用 NewPair(1,2.5)中,两个参数都是没有类型的常数,所以我们进行第二次pass。这次我们将第一个常量设置为int,第二个是 float64。然后,我们尝试用int和float64统一F,统一失败,我们报告了一个编译错误。
如前所述,函数参数类型推断是在约束检查之前完成的。首先,我们使用函数参数类型推断来确定用于函数的类型参数,如果成功,我们接着检查这些类型参数是否实现了约束(如果有的话)。
请注意,在函数参数类型推断成功后,编译器必须检查实参是否可以赋值给形参,就像其他的函数调用一样。
4.5.3. 约束类型推断
注:这部分看不懂的话建议直接看下面代码示例。
约束类型推断允许基于一个类型参数的约束以及另一个类型参数推断出该类型实参。当函数想要获得类型参数列表中某一成员的实际类型,或者函数想要将约束应用于一个使用其他类型参数的类型上,类型推断是可用的。
约束类型推断只能推断出一种类型,该类型的约束类型集中只有一个类型,或者这个类型集中的类型的底层类型完全相同。这两种情况略有不同,如在第一种情况下,类型集只有一个类型,这个类型不需要是它自己的底层类型。无论哪种方式,这个类型称为结构类型,对应的约束称为结构约束。结构类型描述了类型参数所需的结构。结构约束还可以定义方法,但是约束类型推断时会忽略这些方法。当结构类型的定义中使用一个或多个类型参数时,约束类型推断才是可用的。
当至少有一个类型形参的类型实参未知的时候才能使用约束类型推断。
接下来是约束类型推断的算法。
我们首先创建从类型形参到类型实参的映射。我们用所有类型实参已知的类型形参初始化映射。
对于具有结构约束的每个类型形参,我们用结构类型统一类型形参。这将类型形参与其约束相关联。将结果添加到我们维护的映射中。如果统一时发现任何类型形参的关联项,我们也将那些添加到映射。当我们找到一个类型形参的多个关联项时,我们统一每个这样的关联项以生成单个映射条目。如果类型形参与另一类型形参直接关联,这意味着它们都必须与相同类型匹配,我们将每个形参和相关联的实参进行类型统一。如果这些中的任何一个类型统一失败,则约束类型推断失败。
在合并具有结构约束的所有类型形参之后,我们具有所有类型形参到类型实参的映射。假设我们有一个映射了:类型实参 A 对应于类型形参 T。那么在映射中所有类型实参为 T 的地方,我们都替换为 A。重复此过程,直到我们已更换每个类型形参。
当约束类型推断可用时,类型推断如下进行:
-
使用已知类型参数构建映射。 -
应用约束类型推断。 -
使用类型实参应用函数类型推断。 -
再次应用约束类型推断。 -
使用默认类型的任何剩余非类型实参应用函数类型推断。 -
再次应用约束类型推断。
4.5.3.1. 元素约束示例
下面函数中,输入一个数字切片,返回一个相同类型切片,其中每个数字都加倍。
// Double returns a new slice that contains all the elements of s, doubled.
type number interface {
type int, uint32, uint64, float64, float32
}
func Double[E number](s []E) []E {
r := make([]E, len(s))
for i, v := range s {
r[i] = v + v
}
return r
}
但是,如果我们用定义的切片类型调用这个函数,结果将不是定义的类型。下面代码中,V1 的类型是[]int。
// MySlice is a slice of ints.
type MySlice []int
// The type of V1 will be []int, not MySlice.
// Here we are using function argument type inference,
// but not constraint type inference.
var V1 = Double(MySlice{1})
通过引入新类型参数来获取我们想要的结果。
// SC constraints a type to be a slice of some type E.
type SC[E any] interface {
type []E // non-interface type constraint element
}
// DoubleDefined returns a new slice that contains the elements of s,
// doubled, and also has the same type as s.
func DoubleDefined[S SC[E], E number](s S) S {
// Note that here we pass S to make, where above we passed []E.
r := make(S, len(s))
for i, v := range s {
r[i] = v + v
}
return r
}
现在,如果我们使用显式类型的参数,我们可以获得正确的类型。
// The type of V2 will be MySlice.
var V2 = DoubleDefined[MySlice, int](MySlice{1})
函数实参类型推断不足以推断此处的类型参数,因为类型参数E不用于任何输入参数。但是函数实参类型推断和约束类型推断的组合是可以推断出来的。
// The type of V3 will be MySlice.
var V3 = DoubleDefined(MySlice{1})
首先,我们应用函数实参类型推断。我们看到实参的类型是MySlice。函数参数类型推断将 MySlice 匹配类型参数S。
然后,我们继续约束类型推断。我们知道一个类型实参:S 并且具有结构类型约束。我们创建了已知类型参数到类型实参的映射:
{S -> MySlice}
然后,使用该约束类型集中的仅有类型的结构约束与 MySlice 进行类型统一。在这种情况下,结构约束是具有单一元素类型[] E的SC [E],因此我们用[] E统一 S。由于我们已经有一个映射,我们用MySlice统一[] E。由于MySLice定义为[] int,请将E与 int 相关联。我们现在有:
{S -> MySlice, E -> int}
然后,我们用int替换E,所有任务都完成了。DoubleDefined 的类型实参是[MySlice,int]。
此示例显示如何使用约束类型推断为其他的类型参数获取类型实参。在这种情况下,我们可以将S 的元素类型命名为E,然后我们可以将进一步的约束应用于E,在这种情况下需要它是一个int。
4.5.3.2. 指针方法示例
下面的函数需要一个 T,类型 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,
// function argument type inference does not work when calling
// this function.
func FromStrings[T Setter](s []string) []T {
result := make([]T, len(s))
for i, v := range s {
result[i].Set(v)
}
return result
}
现在让我们看看一些调用代码(此示例无效)。
// 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) {
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"})
...
}
这个可以编译但将在运行时panic。问题是,FromStrings 创造了一个类型为[]T 的切片。使用*Settable 实例化时,函数会产生一个 []*Settable 的切片。当 FromStrings 调用 result[i].Set(v)时,会在 result[i]上的指针调用 Set 方法。这个指针是 nil。也就是说,Settable.Set方法的接收者是 nil,所以会产生一个 nil 的 panic。
指针类型*Settable 实现了约束条件,但是代码中想使用非指针类型 Settable。我们需要的是,在 FromStrings 的代码中,它可以传入 Settable 类型,但是可以调用指针方法。我们不能使用 Settable 因为它没有 Set 方法,而且我们不能使用*Settable 因为我们不能创建一个 Settable 的切片。
我们能做的就是把这两个类型都传进去。
// 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[B any] interface {
Set(string)
type *B // non-interface type constraint element
}
// 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 aka PT.
// The Setter2 constraint ensures that PT is a pointer to T.
func FromStrings2[T any, 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 set
// 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}.
...
}
此方法按预期工作,但必须在类型参数中重复设置 Settable。幸运的是,约束类型推断使得它大大简化。使用约束类型推断我们可以写:
func F3() {
// Here we just pass one type argument.
nums := FromStrings2[Settable]([]string{"1", "2"})
// Now nums is []Settable{1, 2}.
...
}
无法避免传递类型参数Settable。但是,给出了类型参数,约束类型推断可以使用*Settable推断类型实参PT。
如之前所示,我们创建了一个已知类型实参映射:
{T -> Settable}
然后,我们用结构约束统一每个类型参数。在这种情况下,我们用单一类型的Setter2 [T]统一PT,就是 *T。映射现在:
{T -> Settable, PT -> *T}
我们现在将 T 替换为 Settable,现在有:
{T -> Settable, PT -> *Settable}
在这个没有改变之后,我们完成了。两个类型的参数都是已知的。
此示例显示了我们如何使用约束类型推断获取其他的类型实参。在这种情况下, PT,即 *T,必须具有一个Set方法。我们可以在不需要调用者明确提及*T的情况下进行此操作。
4.5.3.3. 即使在约束类型推断之后也检查约束
即使使用约束类型推断得出类型实参时,我们仍然必须在确定类型参数后检查约束。
在上面的 FromStrings2 代码中,我们可以通过 Setter2 约束推断出来 PT。但是在推断过程中我们只检查了类型集,并没有检查约束中的方法。我们仍然需要检测是否有对应的方法,即使之前的约束类型推断已经成功了。
例如,下面的无效代码:
// Unsettable is a type that does not have a Set method.
type Unsettable int
func F4() {
// This call is INVALID.
nums := FromString2[Unsettable]([]string{"1", "2"})
...
}
当这个函数被调用了,会进行约束类型推断。约束类型推断会成功,然后推断出来类型实参是[Unsettable, *Unsettable]。只有在我们检查完约束 Setter2[Unsettable]之后这个约束类型推断才是完整的。然而,*Unsettable 没有Set 方法,因此约束检查会失败,该代码无法编译。
4.6. 在约束定义中引用自己
有些时候我们需要在泛型函数的一个类型实参带有一个方法,并且这个方法的参数就是这个类型实参(自己引用自己),这样的用法是很有意义的。例如,这在比较的方法中很常见。(注意:这里讨论的是方法,不是操作符)
假设我们想要写一个 Index 方法,使用 Equal 方法来检测是否找到了需要的值。
// Index returns the index of e in s, or -1 if not found.
func Index[T Equaler](s []T, e T) int {
for i, v := range s {
if e.Equal(v) {
return i
}
}
return -1
}
关于 Equaler 约束条件呢,我们想要写一个约束可以引用输入的类型实参。一个约束不必先定义再使用,我们可以根据这个优点,实现 Equaler 这个约束。下面的接口约束类型中可以引用参数列表中的类型参数。
// Index returns the index of e in s, or -1 if not found.
func Index[T interface { Equal(T) bool }](s []T, e T) int {
// same as above
}
这个版本的 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 {
// The type argument equalInt is shown here for clarity.
// Function argument type inference would permit omitting it.
return Index[equalInt](s, e)
}
在此示例中,当我们把 equalInt传递到Index时,我们检查equalInt是否实现约束 interface { Equal(T) bool }。约束具有类型参数,因此我们将替换类型参数为类型实参,就是 equalInt。这为我们提供了interface { Equal(equalInt) bool }。equalInt 类型具有equal 方法,因此编译成功。
4.7. 类型集相关
让我们现在回到类型集,聊一些仍然值得注意的细节。
4.7.1. 约束中的元素和方法
一个约束可能同时具有约束元素和方法。
// 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
}
类型集的规则决定了约束的意义。type int, int8, int16, int32, int64
的类型集是所有底层是 int 类型的类型集合。String() string的类型集是定义该方法的所有类型的集合。StringableSignedInteger的类型集是这两个类型集的交集。结果是所有底层类型int, int8, int16, int32, int64
之一,且定义了方法String() string 的类型集合。使用StringableSignedInteger作为约束的类型值可以使用任何整数类型允许(+,*等)的操作。它也可能调用String() string方法对值类型p返回一个字符串。
下面代码中是满足约束的一个类型示例:
// 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)
}
4.7.2. 约束中的复合类型
正如我们在一些早期的示例中看到的那样,约束元素可以是类型。
type byteseq interface {
type string, []byte
}
该约束的类型参数可以是字符串或[]byte;使用该约束的函数可以使用字符串和[]byte允许的任何操作。
byteseq 约束允许编写适用于字符串或[]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[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,channel),我们强制执行额外的限制:如果一个操作符能够接收所有的输入类型并产生相同的输出,那么这个操作符是可以使用的。仅当复合类型出现在类型集中时才会施加此附加限制。
// structField is a type constraint whose type set consists of some
// struct types that all have a field named x.
type structField interface {
type struct {
a int
x int
}, struct {
b int
x float64
}, struct {
c int
x uint64
}
}
// This function is INVALID.
func IncrementX[T structField](p *T) {
v := p.x // INVALID: type of p.x is not the same for all types in set
v++
p.x = v
}
// sliceOrMap is a type constraint for a slice or a map.
type sliceOrMap interface {
type []int, map[int]int
}
// Entry returns the i'th entry in a slice or the value of a map
// at key i. This is valid as the result of the operator is always int.
func Entry[T sliceOrMap](c T, i int) int {
// This is either a slice index operation or a map key lookup.
// Either way, the index and result types are type int.
return c[i]
}
// sliceOrFloatMap is a type constraint for a slice or a map.
type sliceOrFloatMap interface {
type []int, map[float64]int
}
// This function is INVALID.
// In this example the input type of the index operation is either
// int (for a slice) or float64 (for a map), so the operation is
// not permitted.
func FloatEntry[T sliceOrFloatMap](c T) int {
return c[1.0] // INVALID: input type is either int or float64.
}
4.7.3. 类型集中的类型参数
约束元素中的类型可以引用约束的类型参数。在此示例中,泛型函数 Map需要两个类型的参数。第一个类型参数需要具有底层类型,该类型是第二个类型参数的切片。第二个类型参数没有约束。
// SliceConstraint is a type constraint that matches a slice of
// the type parameter.
type SliceConstraint[T any] 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[S SliceConstraint[E], E any](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 {
// The type arguments listed explicitly here could be inferred.
v := Map[MySlice, int](s, func(e int) int { return 2 * e })
// Here v has type MySlice, not type []int.
return v
}
4.7.4. 类型转换
在具有两个类型参数 From 和 To 的函数中,一个 From 类型的值可能转换成 To 类型的值,前提是 From 的类型集中的元素都可以转换成 To 类型集中的元素。
这是一个通用的规则:一个泛型函数可以使用的操作符仅限于它的约束条件的类型集中可以使用的操作符。
例如:
type integer interface {
type int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr
}
func Convert[To, From integer](from From) To {
to := To(from)
if From(to) != from {
panic("conversion out of range")
}
return to
}
以上代码允许Convert中的类型转换,因为Go允许将每个整数类型转换为每个其他整数类型。
4.7.5. 非类型常量
有些函数使用非类型常量。如果这个非类型常量满足约束,那它就可以使用。
type integer interface {
type int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr
}
func Add10[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[T integer](s []T) {
for i, v := range s {
s[i] = v + 1024 // INVALID: 1024 not permitted by int8/uint8
}
}
4.7.6. 内嵌约束的类型集
当约束嵌入另一个约束时,外部约束的类型集是所涉及的所有类型集的交集。如果有多种嵌入式类型,则交集具有一个特性,即所有的类型实参都必须满足所有约束元素的条件。
// 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 that is both Addable and Byteseq.
// In other words, just the type set ~string.
type AddableByteseq interface {
Addable
Byteseq
}
嵌入式约束可能目前不会出现在同一行声明中。(有待考证)
4.7.7. union 元素中的接口类型
我们已经表示,union元素的类型集是该声明语句中所有类型类型集的并集。对于大多数类型而言,T的类型集为T本身。但是,对于接口类型(和近似元素)来说不是这种情况。
正如我们之前所说的那样,未嵌入非接口元素的类型的类型集是声明接口的所有方法的所有类型的集合,包括接口类型本身。在union元素中使用这种接口类型会将其类型集添加到union元素的类型集中。
type Stringish interface {
type string, fmt.Stringer
}
Stringish的类型集是字符串类型和实现fmt.Stringer的所有类型。这些类型都可以作为约束的类型实参使用。Stringish 不允许使用任何操作符,因为 string 可以使用操作符,但是 fmt.Stringer 不可以使用,两者取交集就是不可以用操作符了。
4.7.8. 空类型集
可以使用空类型集编写约束。没有类型实参会满足这样的约束,因此任何尝试实例化使用空类型集约束的函数都会失败。一般来说,编译器是不可能检测到所有此类情况的。
// Unsatisfiable is an unsatisfiable constraint with an empty type set.
// No predeclared types have any methods.
// If this used ~int | ~float32 the type set would not be empty.
type Unsatisfiable interface {
type int, float32
String() string
}
总结
Go 泛型入门很简单,但是更复杂的用法的细节比较多。Go1.17 中泛型方法目前不可导出,个人感觉 Go 对添加泛型的意愿不是很强烈,不过具体结果还需要等到 Go1.18 的发布才见分晓。
以下是目前 Go 泛型特性的小结:
-
函数和类型可以具有类型参数,这些参数使用约束定义,该约束是接口类型。 -
约束描述了所需的方法和类型参数所允许的类型。 -
约束描述了类型参数允许的方法和操作。类型推断(函数实参推断、约束类型推断)通常会在使用类型参数调用函数时省略类型参数。
原文始发于微信公众号(crazstom):Go 泛型(3)约束和类型集
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/64114.html