泛型编写教程和建议

140 阅读6分钟

泛型编写教程和建议

1 背景

从开源社区了解,有许多要求在go中添加泛型的声音,因此在1.18中添加了此功能,本文主要翻译和改自此文,感兴趣的同学可以看原文,文章写的更加详细和透彻。本文也会介绍go泛型的高级用法,适用于已经熟悉泛型在Go语言中工作的人。

2 设计

后面将会举例简单的示例来描述泛型设计。

3 泛型使用

3.1 类型参数

通用代码使用到的参数称之为通用类型参数。当使用泛型时将会替代所使用的类型参数。下面举一个简单例子,打印所有切片元素:

func Print(s []T) { // 只是一个例子,不是建议的语法。
	for _, v := range s { 
		fmt.Println(v) 
	} 
}

使用这个方法时,应该如何声明T这个类型参数呢?因此go中引入了[T any]这种写法代表泛型的类型:

func Print[T any](s []T) { 
	for _, v := range s { 
		fmt.Println(v) 
	} 
}

根据这样整理,就可以打印出任何符合该泛型类型的切片了!

3.2 约束

下面呢,我们将例子变的稍微复杂一点,让我们把它变成一个函数,通过在每个元素上调用String()方法,得到结果转为切片返回。

// 这个函数是无效的。
func Stringify[T any](s []T) (ret []string) { 
	for _, v := range s { 
		ret = append(ret, v.String()) // 无效
	} 
	return ret 
}

我们乍一看这样的写法是没错的,但是T属于any类型,也就是任意类型。并不是所有类型都有String()方法,这里需要注意这一点,因此我们需要将类型进行约束才可以使该函数正确运行。

3.2.1 定义约束

go中的接口这个结构其实可以完全满足对于类型的约束,例如下Stringfy接口:

type Stringer interface{
  String() string
}

3.2.2 使用约束

对于泛型函数,可以将约束视为类型参数。因此可以成作如下:

// Stringify 对 s 的每个元素调用 String 方法,
// 并返回结果。
func Stringify[T Stringer](s []T) (ret []string) { 
	for _, v := range s { 
		ret = append(ret, v.String()) // 可以运作
	} 
	return ret 
}

所以上例中的v.String()是可以正常运作的,因为我们将此接口作为其参数约束。

3.4 多种类型参数

上面的例子只是简单的单个参数,扩展一下就可以得到多种类型参数的方式,如下:

// Print2 有两个any类型的两个不同参数。
func Print2[T1, T2 any](s1 []T1, s2 []T2) { ... }

以及

// Print2Same 有一个any类型的两个不同参数。
func Print2Same[T any](s1 []T, s2 []T) { ... }

这样就可以让泛型有了更好的扩展性,满足我们日常开发更多的需要。

3.5 通用类型

除了泛型函数,我们还需要支持泛型类型,这里建议扩展类型来获取类型参数:

type Vector[T any] []T // 声明泛型类型参数

func (v *Vector[T]) Push(x T) { // 这里不需要声明约束,因为上述定义时已经约束,这里只需要使用该类型即可
	*v = append(*v, x)
}

func main() {
	var v Vector[int]
	v.Push(1)
	fmt.Println(v)
}

泛型同样适用于struct类型中,但是需要注意一点的是,如果自身引用泛型,必须按照定义的顺序进行,否则可能出现无限递归的问题:

// List 是一个 T 类型值的链表
type List[T any] struct { 
	next *List[T] // 这个对 List[T] 的引用是 OK 
	val T 
} 

// 这个类型是无效的。
type P[T1, T2 any] struct { 
	F *P[T2, T1] // 无效;必须是 [T1, T2] ,否则实例化可能出现问题
}

3.6 操作符

正如我们所见,我们使用接口类型作为约束,接口类型仅仅提供了一组方法而已。然而泛型方法不能表达我们需要的一切,如下我们需要返回切片中最小的元素:

// 这个函数是无效的。
func Smallest[T any](s []T) T { 
	r := s[0] // 如果切片为空
	for _, v := range s[1:] { 
		if v < r { // INVALID 
			r = v 
		} 
	}
	return r 
}

上面例子的写法是不成立的,因为Tany类型,并不能通过操作符进行比较大小。因此我们需要定义约束集合来限制T的类型符合我们需要。这里必须要有序比较才可以,comparable是不可行的,它只用于==!=的比较。这里可以使用的类型有int、float等ordered类型,因此我们可以将上述修改如下:

// 这个函数是无效的。
func Smallest[T interface{int|int8|~float}](s []T) T { 
	r := s[0] // 如果切片为空
	for _, v := range s[1:] { 
		if v < r { 
			r = v 
		} 
	}
	return r 
}

这样的写法可以满足我们要求。

我们还可以定义一个约束类型集合来拱我们自己使用,具体例子如下:

type PredeclaredSignedInteger interface {
	int | int8 | int16 | int32 | int64
}

3.7 约束中的可比较类型

前面我们提到,运算符只能用于语言预先声明的类型的规则有两个例外。例外是==and !=,它们允许用于结构、数组和接口类型。这些足够有用,我们希望能够编写一个接受任何可比较类型的约束。

// Index 返回 x 在 s 中的索引,如果没有找到则返回 -1。
func Index[T comparable](s []T, x T) int { 
	for i, v := range s { 
		// v 和 x 是类型 T,具有可比较
		// 约束,所以我们可以在这里使用 == . 
		if v == x {
			return i 
		} 
	}
	return -1 
}

由于comparable是一个约束类型,因此可以嵌入到接口中

type ComparableHaser interface{
  comparable
  Hash() unitptr
}

ComparableHaser可以通过Hash函数得到的值来进行比较是否相等来判断两个类型是否相等。

3.8 相互引用类型参数

类型之间相互引用在开发过程中可能会出现,例如获取连接A点的所有边或者获取A边的两端点,这就可能需要相互引用的参数,如下为示例:

type NodeConstraint[Edge any] interface {
	Edges() []Edge
}

type EdgeConstraint[Node any] interface {
	Nodes() (from, to Node)
}

// 图是由节点和边组成的图。两者相互引用
type Graph[Node NodeConstraint[Edge], Edge EdgeConstraint[Node]] struct {
}
type GNode struct{}
type GEdge struct{}

func (g *GNode) Edges() []*GEdge {
	return nil
}
func (g *GEdge) Nodes() (from, to *GNode) {
	return &GNode{}, &GNode{}
}
func New[Node NodeConstraint[Edge], Edge EdgeConstraint[Node]](nodes []Node) *Graph[Node, Edge] {
	fmt.Println(nodes)
	return nil
}

func main() {
	New[*GNode, *GEdge]([]*GNode{})//调用相互引用类型时,必须声明两个类型显示调用,这里编译器无法自动推断
}

3.9 类型推断

在许多情况下,我们可以使用类型推断来避免显示的写出类型参数。并且可以根据已知的类型参数推断出未知的类型参数。例如:

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 type arguments, and let both be inferred.
r = Map(s, f)

如果在没有指定所有类型参数的情况下使用泛型函数或者类型,那么就无法推断出类型,则会出现错误。