Go 语言泛型编程的用处之通用类型切片

1,613 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 12 天,点击查看活动详情

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 函数。

参考链接: