解构Golang类型参数
目录
英文原文⌗
https://go.dev/blog/deconstructing-type-parameters
笔记⌗
slices.Clone
函数很简单,它可以拷贝任何类型slice。
func Clone[S ~[]E, E any](s S) S {
return append(s[:0:0], s...)
}
s[:0:0]
分配了一个新的底层数组,来容纳s
的值。这篇文章主要是探讨为什么这个泛型函数的方法签名是这么定义的。
简单的Clone方法⌗
如果我们拷贝一个泛型的slice,可能会怎么写?
func Clone1[E any](s []E) []E{
// 忽略方法体
}
该泛型函数Clone1
有一个单独的类型参数E
。它的输入是一个E
的slice,输出是一个E
的slice,而E
代表任何类型。看过去很直观。
但是有个问题Named slice
虽然在Go中比较少用,但是确认有人这么写。
// 以下是string的slice,带有个方法String
type MySlice []string
func (s MySlice) String() string {
return strings.Join(s, "+")
}
如果我们想要拷贝一个MySlice
并获取一个printable版本,该版本的输出需要先排序好。
func PrintSorted(ms MySlice) string{
c := Clone1(ms)
slices.Sort(c)
return c.String() // 这里会编译失败
// c.String undefined (type []string has no field or method String)
}
根据Go assignment rules,go允许我们将类型为MySlice
的值传递给类型为[]string
的参数,所以调用Clone1
是可以的。但是Clone1
将会返回一个类型为[]string
的值,而不是类型为MySlice
的值。而类型[]string
并没有String
方法,所以编译失败了。实际上Clone1
返回的类型和输入的类型并不相同。
弹性的Clone方法⌗
为了解决这个问题,需要编写一个Clone
返回值和输入参数一样的类型。这样我们才能说,我们Clone
了一个MySlice
,它就会返回一个MySlice
.
所以Clone可能这么写
// 此处?表示可能的占位符,并不是实际的泛型约束
func Clone2[S ?](s S) S
此时编译器会提示错误信息type parameters must be named
。
由于我们知道我们想要操作的是个slice
,且slice中的值的类型可以是任意类型,姑且称之为E
。那么方法签名就会变成
func Clone3[S []E](s S) S
此时编译器会提示错误信息undefined: EcompilerUndeclaredName
。
以上签名还是非法的,因为没有声明E
。由于E
可以是任意类型,方法签名就可以变为
func Clone4[S []E, E any](s S) S
该方法签名本身编译不会报错,但是当我们调用Clone4
的时候,编译器会提示错误信息MySlice does not satisfy []string (possibly missing ~ for []string in []string)
。
func PrintSorted(ms MySlice) string {
// MySlice does not satisfy []string (possibly missing ~ for []string in []string)
c := Clone4(ms)
slices.Sort(c)
return c.String()
}
这个提示告诉我们,MySlice
不符合约束[]E
。因为[]E
作为一个约束,只允许slice
类型本身,比如[]string
。 它并不允许一个命名的类型(named type)比如MySlice
。
底层类型约束(Underlying type constraints)⌗
按照错误提示,我们可以添加上~
,那么方法签名就变成了
func Clone5[S ~[]E, E any](s S) S
加上~
之后有什么区别呢?
带上~
后,意味着该类型参数S可以是任意底层类型是一个slice
的类型,比如MySlice
。可以查看underlying types spec。
为什么Go语法需要~
呢?我们总要允许传递MySlice
,因此为什么不让([]E
)默认就是底层类型呢?或者,如果说在我们需要明确的类型匹配的时候,使用类似=[]E
的语法,只精确地允许该类型。
为了解释这个问题,我们首先发现一个type parameter
列表如[T ~MySlice]
是没有意义的。这是因为MySlice
不是任何其他类型的underlying type
。例如,如果我们有一个定义如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]
的含义,这表示底层类型为MySlice
的所有类型?还是MySlice
本身呢?
我们可以禁用[S MySlice]
这样的Named type,或者我们可以说[S MySlice]
只匹配MySlice
,但是这两种方式在碰到预定义的类型的时候都会有问题。一个预定义类型,如int
是它本身的底层类型。我们想要允许人们可以编写约束,可以接受任何底层类型为int
的类型参数。按照目前的Go语言,可以这么写[T ~int]
。如果我们不需要~
,我们仍然需要有一种方式来表达任何底层类型为int的类型。最自然的方式是[T int]
。这也就意味着[T MySlice]
和[T int]
将会有不同的行为,即便他俩看起来很类似。
我们可能可以说[S MySlice]
匹配一种类型,该类型表示底层类型和MySlice
的底层类型是一样的,但是这让[S MySlice]
变得没有必要且让人困惑。
所以最好还是要有~
,在我们需要匹配底层类型,而非该类型本身的时候,可以更清晰一点。
类型推断(Type inference)⌗
既然我们已经解释清楚了slices.Clone
这个方法签名,那么我们可以看看实际使用slices.Clone
的时候,type inference怎么帮助我们简化代码。
func Clone[S ~[]E, E any](s S) S
一个slices.Clone
的调用会传递一个slice给参数s
。简单的type inference将会让编译器会从传递给该函数的参数的类型来推导出类型参数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
,然后通过另个另外两个类型参数K
和V
来定义该类型。
由于Go类型可以从components types构建,我们也可以使用类型参数来解构这些类型,并按照我们的需要约束它们。