写点什么

Go 语言泛型编程之切片

作者:宇宙之一粟
  • 2022-10-12
    中国香港
  • 本文字数:1941 字

    阅读完需:约 6 分钟

Go 语言泛型编程之切片

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


  • 利用类型参数来写出真实世界的代码。

  • 泛型在实际中有什么用途呢?

  • 在没有泛型之前 Go 不能实现什么样的代码?

Go 切片 Slice

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


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


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


在切片中寻找元素

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


例如,我们不能写一个函数,接收一个任意类型的切片,并确定该切片是否包含一个给定的元素。相反,我们需要写一个个函数,接收你需要的特定类型,如 []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 函数。


参考链接:


发布于: 刚刚阅读数: 5
用户头像

宇宙古今无有穷期,一生不过须臾,当思奋争 2020-05-07 加入

🏆InfoQ写作平台-签约作者 🏆 混迹于江湖,江湖却没有我的影子 热爱技术,专注于后端全栈,轻易不换岗 拒绝内卷,工作于外企开发,弹性不加班 热衷分享,执着于阅读写作,佛系不水文 同名公众号:《宇宙之一粟》

评论

发布
暂无评论
Go 语言泛型编程之切片_Go_宇宙之一粟_InfoQ写作社区