Go 博客

解构类型参数

Ian Lance Taylor
2023 年 9 月 26 日

slices 包函数签名

slices.Clone 函数很简单:它会复制任意类型的 slice。

func Clone[S ~[]E, E any](s S) S {
    return append(s[:0:0], s...)
}

之所以能这样工作,是因为向一个容量为零的 slice 添加元素会分配一个新的底层数组。函数体比函数签名短,一部分是因为函数体很短,但也因为签名很长。在这篇博客文章中,我们将解释为什么签名会这样写。

简单的 Clone

我们将从编写一个简单的泛型 Clone 函数开始。这不是 slices 包中的那个。我们想要接收任意元素类型的 slice,并返回一个新的 slice。

func Clone1[E any](s []E) []E {
    // body omitted
}

泛型函数 Clone1 有一个单独的类型参数 E。它接收一个参数 s,其类型是 E 类型的 slice,并返回相同类型的 slice。对于熟悉 Go 中泛型的人来说,这个签名很简单。

然而,有一个问题。具名 slice 类型在 Go 中并不常见,但人们确实会使用它们。

// MySlice is a slice of strings with a special String method.
type MySlice []string

// String returns the printable version of a MySlice value.
func (s MySlice) String() string {
    return strings.Join(s, "+")
}

假设我们想复制一个 MySlice,然后获取其可打印版本,但要将字符串按排序顺序排列。

func PrintSorted(ms MySlice) string {
    c := Clone1(ms)
    slices.Sort(c)
    return c.String() // FAILS TO COMPILE
}

不幸的是,这行不通。编译器会报告一个错误:

c.String undefined (type []string has no field or method String)

如果我们手动实例化 Clone1,通过将类型参数替换为类型实参,我们可以看到问题所在。

func InstantiatedClone1(s []string) []string

Go 的 赋值规则 允许我们将 MySlice 类型的值传递给 []string 类型的参数,因此调用 Clone1 是没问题的。但 Clone1 会返回一个 []string 类型的值,而不是 MySlice 类型的值。[]string 类型没有 String 方法,因此编译器会报告一个错误。

灵活的 Clone

为了解决这个问题,我们必须编写一个 Clone 版本,它返回的类型与其参数的类型相同。如果我们能做到这一点,那么当我们用 MySlice 类型的值调用 Clone 时,它将返回 MySlice 类型的结果。

我们知道它看起来应该像这样。

func Clone2[S ?](s S) S // INVALID

这个 Clone2 函数返回一个类型与其参数相同的返回值。

这里我将约束写成了 ?,但这只是一个占位符。为了让它工作,我们需要编写一个约束,允许我们编写函数体。对于 Clone1,我们可以简单地使用 any 作为元素类型的约束。对于 Clone2,这行不通:我们希望要求 s 是一个 slice 类型。

既然我们知道我们想要一个 slice,那么 S 的约束必须是一个 slice。我们不关心 slice 的元素类型是什么,所以我们把它叫做 E,就像我们在 Clone1 中做的那样。

func Clone3[S []E](s S) S // INVALID

这仍然是无效的,因为我们还没有声明 EE 的类型实参可以是任何类型,这意味着它本身也必须是一个类型参数。由于它可以是任何类型,所以它的约束是 any

func Clone4[S []E, E any](s S) S

这已经接近完成了,至少可以编译通过,但我们还没有完全成功。如果我们编译这个版本,在调用 Clone4(ms) 时会得到一个错误。

MySlice does not satisfy []string (possibly missing ~ for []string in []string)

编译器告诉我们,不能将 MySlice 作为类型参数 S 的类型实参,因为 MySlice 不满足 []E 的约束。这是因为 []E 作为约束只允许 slice 类型字面量,比如 []string。它不允许像 MySlice 这样的具名类型。

底层类型约束

正如错误消息所暗示的,答案是加上一个 ~

func Clone5[S ~[]E, E any](s S) S

重申一下,编写类型参数和约束 [S []E, E any] 意味着 S 的类型实参可以是任何未命名的 slice 类型,但它不能是定义为 slice 字面量的具名类型。编写 [S ~[]E, E any],加上 ~,意味着 S 的类型实参可以是任何底层类型是 slice 类型的类型。

对于任何具名类型 type T1 T2T1 的底层类型是 T2 的底层类型。像 int 这样的预声明类型或像 []string 这样的类型字面量的底层类型就是类型本身。有关确切的细节,请参见语言规范。在我们的例子中,MySlice 的底层类型是 []string

由于 MySlice 的底层类型是 slice,我们可以将 MySlice 类型的值传递给 Clone5。您可能已经注意到,Clone5 的签名与 slices.Clone 的签名相同。我们终于达到了我们的目标。

在我们继续之前,让我们讨论一下为什么 Go 语法需要 ~。看起来我们总是想允许传递 MySlice,那么为什么不将其设为默认呢?或者,如果我们想支持精确匹配,为什么不颠倒过来,这样 []E 的约束就允许具名类型,而像 =[]E 这样的约束只允许 slice 类型字面量呢?

为了解释这一点,我们首先观察到像 [T ~MySlice] 这样的类型参数列表没有意义。这是因为 MySlice 并不是任何其他类型的底层类型。例如,如果我们有一个定义,如 type MySlice2 MySlice,那么 MySlice2 的底层类型是 []string,而不是 MySlice。因此,[T ~MySlice] 要么不允许任何类型,要么与 [T MySlice] 相同,只匹配 MySlice。无论哪种方式,[T ~MySlice] 都没有用处。为了避免这种混淆,语言禁止 [T ~MySlice],编译器会产生一个类似的错误:

invalid use of ~ (underlying type of MySlice is []string)

如果 Go 不要求波浪号,这样 [S []E] 就会匹配任何底层类型为 []E 的类型,那么我们就必须定义 [S MySlice] 的含义。

我们可以禁止 [S MySlice],或者说 [S MySlice] 只匹配 MySlice,但这两种方法在处理预声明类型时都会遇到麻烦。像 int 这样的预声明类型,其底层类型就是它本身。我们希望允许人们编写接受任何底层类型为 int 的类型实参的约束。在今天的语言中,他们可以通过编写 [T ~int] 来做到这一点。如果我们不要求波浪号,我们仍然需要一种方法来说“任何底层类型为 int 的类型”。自然的方法是 [T int]。这意味着 [T MySlice][T int] 的行为会不同,尽管它们看起来非常相似。

我们或许可以这样说:[S MySlice] 匹配任何底层类型为 MySlice 的底层类型的类型,但这使得 [S MySlice] 变得不必要且令人困惑。

我们认为要求使用 ~ 并清楚地区分何时匹配底层类型而不是类型本身更好。

类型推断

现在我们已经解释了 slices.Clone 的签名,让我们看看类型推断如何实际简化 slices.Clone 的使用。记住,Clone 的签名是:

func Clone[S ~[]E, E any](s S) S

调用 slices.Clone 时,会将一个 slice 传递给参数 s。简单的类型推断将允许编译器推断出类型参数 S 的类型实参就是传递给 Clone 的 slice 的类型。然后,类型推断足够强大,可以知道 E 的类型实参是传递给 S 的类型实参的元素类型。

这意味着我们可以写:

    c := Clone(ms)

而无需写:

    c := Clone[MySlice, string](ms)

如果我们不调用 Clone 而是引用它,我们确实需要为 S 指定类型实参,因为编译器没有什么可以用来推断它的。幸运的是,在这种情况下,类型推断能够从 S 的参数推断出 E 的类型实参,而我们不必单独指定它。

也就是说,我们可以写:

    myClone := Clone[MySlice]

而无需写:

    myClone := Clone[MySlice, string]

解构类型参数

我们在这里使用的通用技术,即通过另一个类型参数 E 来定义一个类型参数 S,是一种在泛型函数签名中解构类型的方法。通过解构类型,我们可以命名并约束类型的各个方面。

例如,这是 maps.Clone 的签名。

func Clone[M ~map[K]V, K comparable, V any](m M) M

slices.Clone 类似,我们为参数 m 的类型使用一个类型参数,然后使用另外两个类型参数 KV 来解构类型。

maps.Clone 中,我们将 K 约束为可比较的,这对于 map 的键类型是必需的。我们可以根据需要约束组件类型。

func WithStrings[S ~[]E, E interface { String() string }](s S) (S, []string)

这表示 WithStrings 的参数必须是一个 slice 类型,并且其元素类型具有 String 方法。

由于所有 Go 类型都可以由组件类型构建而来,因此我们始终可以使用类型参数来解构这些类型并根据需要进行约束。

下一篇文章: 你一直想知道的关于类型推断的一切 - 以及更多
上一篇文章: 修复 Go 1.22 中的 for 循环
博客索引