Go 现在都支持泛型了,我们该怎么利用泛型的特点。
- 利用类型参数来写出真实世界的代码。
- 泛型在实际中有什么用途呢?
- 在没有泛型之前 Go 不能实现什么样的代码?
Go 切片 Slice
我们先来看一下切片,切片在 Go 中并不是简单的数组,而是一个结构体,其定义如下:
用图示来看,一个空的slice的表现如下:
在切片中寻找元素
虽然我们通常可以编写特定程序所需的特定代码,但是在泛型之前,在任意容易类型(如切片)上编写函数并不容易。
例如,我们不能写一个函数,接收一个任意类型的切片,并确定该切片是否包含一个给定的元素。相反,我们需要写一个个函数,接收你需要的特定类型,如 []int
或 []string
。
但这会很无聊,因为无论切片元素的类型如何,其逻辑都是完全一样的。有了泛型之后,我们就可以通过只写一次这个函数,适用于所有类型。
现在来看一个例子:
对于如上的 Contains
函数:对于任意可比较的类型 E,Contains[E]
接收某个类型的切片 []E
,和同一类型的元素值 v
,并返回一个 bool 值,如果该元素在切片中,则返回 true, 否则返回 false。
类型参数 E
是受限制的,意味着我们不能从字面上接受任何类型,因为我们需要比较元素。如果我们不能确定 v == vs
,我们就无法判断这两个元素是否匹配。
而在 Go 中,并不是每一种数据类型都能用等号 ==
的方式进行比较。例如,切片和 map 都不具有直接可比性,结构体如果包含切片或 map 等字段,也不具有可比性。
所以 Go 提供了一个预定义的类型约束,名为 comparable
,它限制了 Contains
所允许的类型,使其只能与 ==
操作符一起工作。
一个需要比较类型参数值的通用函数必须至少受到可比性的约束,就像本例中一样。
切片反转
我们完全可以编写泛型函数,对字面上任何类型的片子进行操作,在这种情况下,类型约束将只是任意的。例如,如果我们不需要比较元素,那么我们就不会被限制在只有可比较的类型。
例如,我们想反转一个切片,也就是说,从某个元素类型 E
中抽取一个切片,然后产生一个包含相同元素,但是元素顺序相反的切片。
可以尝试写如下的代码:
这次,我们就不需要 comparable
,因为对切片进行反转并不需要做任何的比较。我们所需要做的就是在给定的切片基础上,向后循环,然后将每个元素添加到 result
切片上。
具体的实现并不重要,而且这个实现可能并不是最高效的,甚至也太好理解。关键的是,如果没有泛型,不为特定的元素类型重复整个 reverse
函数,根本就不能实现相同的效果。
切片排序
对切片进行排序是另一个例子,如果没有泛型,可能会有点复杂。
标准库的 sort.Slice
API 要求用户传入自己的函数来确定两个元素的大小。
但是,当我们对像 int 很容易比较大小的类型进行排序时,必须传入这样一个函数似乎很愚蠢,而这通常就是情况。
Go 完全知道如何用 <
来比较两个整数,那么为什么我们要写一个函数来做这个呢?好吧,你知道为什么,但这并不意味着它就不令人讨厌了。
现在我们可以写一个通用的 Sort
函数,它没有如此尴尬的 API 。
就像前面的 Contains
例子一样,我们在这里需要一个类型约束。并非每一种 Go 类型都可以使用 <
操作符:例如,结构体就不可以。不过,其值可以明确排序的数据类型,被称为有序类型。所以我们使用标准库中的 constraints.Ordered
来实现这个限制。
同样,这肯定不是一个模型的实现--它背后使用了 sort.Slice
,这个函数很慢,因为它使用了反射。这在泛型中是不必要的,我们可以直接实现一些高效的排序算法,如快速排序 Quicksort
。
这个 Sort
函数的重要之处不是代码是否好--它并不好--而是我们可以为任何有序类型直接编写它。为了不让无序类型的人感到被遗弃,我们可以提供一个版本,让用户可以像以前一样传入自己的 Less
函数。
参考链接: