Go 语言泛型编程之切片

时间:2022-10-13 11:10:26

Go 语言泛型编程之切片

Go 现在都支持泛型了,我们该怎么利用泛型的特点。

  • 利用类型参数来写出真实世界的代码。
  • 泛型在实际中有什么用途呢?
  • 在没有泛型之前 Go 不能实现什么样的代码?

Go 切片 Slice

我们先来看一下切片,切片在 Go 中并不是简单的数组,而是一个结构体,其定义如下:

type slice struct {
array unsafe.Pointer // 指向存放数据的数组指针
len int // 长度
cap int // 容量
}

用图示来看,一个空的slice的表现如下:

Go 语言泛型编程之切片

在切片中寻找元素

虽然我们通常可以编写特定程序所需的特定代码,但是在泛型之前,在任意容易类型(如切片)上编写函数并不容易。

例如,我们不能写一个函数,接收一个任意类型的切片,并确定该切片是否包含一个给定的元素。相反,我们需要写一个个函数,接收你需要的特定类型,如 ​​[]int​​ 或 ​​[]string​​ 。

但这会很无聊,因为无论切片元素的类型如何,其逻辑都是完全一样的。有了泛型之后,我们就可以通过只写一次这个函数,适用于所有类型。

现在来看一个例子:

func Contains[E comparable](s []E, v E) bool {

for _, vs := range s {

if v == vs {

return true

}

}



return false

}

对于如上的 ​​Contains​​ 函数:对于任意可比较的类型 E,​​Contains[E]​​ 接收某个类型的切片 ​​[]E​​,和同一类型的元素值 ​​v​​,并返回一个 bool 值,如果该元素在切片中,则返回 true, 否则返回 false。

类型参数 ​​E​​ 是受限制的,意味着我们不能从字面上接受任何类型,因为我们需要比较元素。如果我们不能确定 ​​v == vs​​ ,我们就无法判断这两个元素是否匹配。

而在 Go 中,并不是每一种数据类型都能用等号 ​​==​​ 的方式进行比较。例如,切片和 map 都不具有直接可比性,结构体如果包含切片或 map 等字段,也不具有可比性。

所以 Go 提供了一个预定义的类型约束,名为 ​​comparable​​,它限制了 ​​Contains​​ 所允许的类型,使其只能与 ​​==​​ 操作符一起工作。

一个需要比较类型参数值的通用函数必须至少受到可比性的约束,就像本例中一样。

切片反转

我们完全可以编写泛型函数,对字面上任何类型的片子进行操作,在这种情况下,类型约束将只是任意的。例如,如果我们不需要比较元素,那么我们就不会被限制在只有可比较的类型。

例如,我们想反转一个切片,也就是说,从某个元素类型 ​​E​​ 中抽取一个切片,然后产生一个包含相同元素,但是元素顺序相反的切片。

可以尝试写如下的代码:

func Reverse[E any](s []E) []E {



result := make([]E, 0, len(s))

for i := len(s) - 1; i >= 0; i-- {

result = append(result, s[i])

}



return result

}

这次,我们就不需要 ​​comparable​​ ,因为对切片进行反转并不需要做任何的比较。我们所需要做的就是在给定的切片基础上,向后循环,然后将每个元素添加到 ​​result​​ 切片上。

具体的实现并不重要,而且这个实现可能并不是最高效的,甚至也太好理解。关键的是,如果没有泛型,不为特定的元素类型重复整个 ​​reverse​​ 函数,根本就不能实现相同的效果。

切片排序

对切片进行排序是另一个例子,如果没有泛型,可能会有点复杂。

标准库的 ​​sort.Slice​​ API 要求用户传入自己的函数来确定两个元素的大小。

sort.Slice(s, func(i, j int) bool
{
return s[i] < s[j]
})

但是,当我们对像 int 很容易比较大小的类型进行排序时,必须传入这样一个函数似乎很愚蠢,而这通常就是情况。

Go 完全知道如何用 ​​<​​ 来比较两个整数,那么为什么我们要写一个函数来做这个呢?好吧,你知道为什么,但这并不意味着它就不令人讨厌了。

现在我们可以写一个通用的 ​​Sort​​ 函数,它没有如此尴尬的 API 。

func Sort[E constraints.Ordered](s []E) []E {
result := make([]E, len(s))

copy(result, s)

sort.Slice(result, func(i, j int) bool {
return result[i] < result[j]
})
return result
}

就像前面的 ​​Contains​​ 例子一样,我们在这里需要一个类型约束。并非每一种 Go 类型都可以使用 ​​<​​ 操作符:例如,结构体就不可以。不过,其值可以明确排序的数据类型,被称为有序类型。所以我们使用标准库中的 ​​constraints.Ordered​​ 来实现这个限制。

同样,这肯定不是一个模型的实现--它背后使用了 ​​sort.Slice​​ ,这个函数很慢,因为它使用了反射。这在泛型中是不必要的,我们可以直接实现一些高效的排序算法,如快速排序 ​​Quicksort​​ 。

这个 ​​Sort​​ 函数的重要之处不是代码是否好--它并不好--而是我们可以为任何有序类型直接编写它。为了不让无序类型的人感到被遗弃,我们可以提供一个版本,让用户可以像以前一样传入自己的 ​​Less​​ 函数。

参考链接: