了解接口泛型的核心类型
1 简介
核心类型的概念(有时)与底层类型不同,其存在是为了一些与泛型相关的构造的好处。当我们将来开始使用核心类型时,这将更有意义。因此,这里只会轻松浏览描述,而无需深入探讨规范中提供的更多细节。
2 接口和泛型的核心类型
这种基于类型集的方法非常灵活,符合原始泛型提案的意图: 如果涉及泛型类型的作数的作对相应的 类型约束。 为了简化与执行有关的问题,因为我们知道我们以后可以放宽规则, 这种方法并不是普遍选择的。 相反,例如,对于 Send 语句,规范规定
通道表达式的核心类型必须是通道,通道方向必须允许发送作, 并且要发送的值的类型必须可分配给通道的元素类型。
这些规则基于核心类型的概念,其定义大致如下:
如果类型不是类型参数,则其核心类型只是其基础类型。
如果类型是类型参数,则核心类型是类型参数类型集中所有类型的单个基础类型。 如果类型集具有不同的基础类型,则核心类型不存在。
例如,有一个核心类型(),但上面的接口没有核心类型。 让事情变得更加复杂的是,当涉及到通道作和某些内置调用 时,上述定义 的核心类型限制性太强。 实际规则具有允许不同通道方向和包含 和 类型的类型集的调整。
这种方法存在各种问题:
因为核心类型的定义必须导致不同语言特征的声音类型规则, 它对特定作的限制过多。
例如,切片表达式的 Go 1.24 规则确实依赖于核心类型, 因此,不允许对 constrained 的类型作数进行切片,即使 它可能是有效的。SConstraint
当试图理解特定的语言特征时,人们可能必须学习 核心类型,即使考虑非泛型代码也是如此。
同样,对于切片表达式,语言规范谈到了切片作数的核心类型, 而不仅仅是声明作数必须是数组、切片或字符串。
后者更直接、更简单、更清晰,并且不需要了解另一个可能 与具体情况无关。
因为存在核心类型的概念,所以索引表达式的规则,以及(以及其他), 它们都避开了核心类型,在语言中显示为例外,而不是规范。
反过来,核心类型会导致诸如问题 #48522 之类的提案,该提案将允许选择器访问类型集的所有元素共享的字段。
如果没有核心类型,该特征就会成为非泛型普通规则的自然而有用的结果 现场访问
非接口类型始终具有核心类型,该类型 是非接口类型的基础类型。 通常,在使用自定义泛型时,我们不关心这种情况。
接口类型可能具有核心类型,也可能没有核心类型。
一般来说,如果所有类型在类型集的接口类型(约束) 共享相同的基础类型, 则相同的底层类型称为接口类型的核心类型。
如果 then 接口类型类型集中的类型不共享相同的基础类型 但它们都是共享相同元素类型的通道类型,并且它们中的所有定向通道都具有相同的方向, 则接口类型的核心类型是定向通道类型或 ,具体取决于存在的方向通道的方向。
3 实例接口的核心类型
对于上述两种情况以外的情况,接口类型没有核心类型。
例如,在下面的代码中,第一组中显示的每种类型 具有核心类型(在尾部注释中指示),但 第二组都没有核心类型。
type (
Age int // int
AgeC interface {Age} // int
AgeOrInt interface {Age | int} // int
Ints interface {~int} // int
AgeSlice []Age // []Age
AgeSlices interface{~[]Age} // []Age
AgeSliceC interface {[]Age | AgeSlice} // []Age
C1 interface {chan int | chan<- int} // chan<- int
C2 interface {chan int | <-chan int} // <-chan int
)
type (
AgeOrIntSlice interface {[]Age | []int}
OneParamFuncs interface {func(int) | func(int) bool}
Streams interface {chan int | chan Age}
C3 interface {chan<- int | <-chan int}
)
许多操作需要约束的类型参数具有核心类型。
具有核心类型的接口示例:
type Celsius float32
type Kelvin float32
interface{ int } // int
interface{ Celsius|Kelvin } // float32
interface{ ~chan int } // chan int
interface{ ~chan int|~chan<- int } // chan<- int
interface{ ~[]*data; String() string } // []*data
没有核心类型的接口示例:
interface{} // no single underlying type
interface{ Celsius|float64 } // no single underlying type
interface{ chan int | chan<- string } // channels have different element types
interface{ <-chan int | chan<- int } // directional channels have different directions
为了简化描述,有时我们还将约束的核心类型称为约束 类型参数作为类型参数的核心类型。
请注意,从未来的 Go 版本开始,核心类型概念可能会被删除,以便在未来的 Go 版本中消除下面列出的许多限制。
函数必须具有可调用的核心类型
例如,目前(Go 1.22),在下面的代码中,函数和不编译,位函数做。 原因是 和 泛型函数中的类型参数 两者都没有核心类型,甚至foobartagFfoobar
但是泛型函数中的 type 参数确实具有。Ftag
func foo[F func(int) | func(any)] (f F, x int) {
f(x) // error: invalid operation: cannot call non-function f
}
func bar[F func(int) | func(int)int] (f F, x int) {
f(x) // error: invalid operation: cannot call non-function f
}
type Fun func(int)
func tag[F func(int) | Fun] (f F, x int) {
f(x) // okay
}
目前尚不清楚该规则是否会在未来的 Go 版本中放宽。
复合文本中的类型文本必须具有核心类型
例如,当前(Go 1.22),在以下代码片段中, 函数和编译没问题,但其他的不行。foobar
func foo[T ~[]int] () {
_ = T{}
}
type Ints []int
func bar[T []int | Ints] () {
_ = T{} // okay
}
func ken[T []int | []string] () {
_ = T{} // error: invalid composite literal type T
}
func jup[T [2]int | map[int]int] () {
_ = T{} // error: invalid composite literal type T
}
元素索引操作要求容器操作数的类型集不能同时包含映射和非映射
如果类型集中的所有类型都是映射,则其基础类型必须相同 (换句话说,操作数的类型必须具有核心类型)。 否则,它们的元素类型必须相同。 字符串的元素被视为值。byte
例如,目前(Go 1.22),在下面的代码片段中,只有函数和编译没问题。foobar
func foo[T []byte | [2]byte | string](c T) {
_ = c[0] // okay
}
type Map map[int]string
func bar[T map[int]string | Map](c T) {
_ = c[0] // okay
}
func lag[T []int | map[int]int](c T) {
_ = c[0] // invalid operation: cannot index c
}
func vet[T map[string]int | map[int]int](c T) {
_ = c[0] // invalid operation: cannot index c
}
该限制可能会在将来的 Go 版本中删除 (只是我的希望,事实上我不确定这一点)。
如果索引表达式的类型是类型参数, 则其类型集中的所有类型都必须是整数。 以下函数编译正常。
func ind[K byte | int | int16](s []int, i K) {
_ = s[i] // okay
}
(看起来当前的 Go 规范在这一点上是不正确的。 规范要求索引表达式必须具有核心类型。
(子)切片操作要求容器操作数具有核心类型
例如,目前(Go 1.22),以下两个函数都编译失败, 即使子切片操作对相应类型集中的所有类型都有效。
func foo[T []int | [2]int](c T) {
_ = c[:] // invalid operation: cannot slice c: T has no core type
}
func bar[T [8]int | [2]int](c T) {
_ = c[:] // invalid operation: cannot slice c: T has no core type
}
该限制可能会在将来的 Go 版本中删除 (再次,只是我的希望,事实上我不确定这一点)。
此规则有一个例外。如果容器操作数的类型集 只包含字符串和字节切片类型,则不需要具有核心类型。 例如,以下函数编译正常。
func lol[T string | []byte](c T) {
_ = c[:] // okay
}
与元素索引操作相同,如果索引表达式的类型是类型参数, 则其类型集中的所有类型都必须是整数。
在循环中,远程容器需要具有核心类型for-range
例如,当前(Go 1.22),在以下代码中, 只有最后两个函数,和 ,编译正常。dot1dot2
func values[T []E | map[int]E, E any](kvs T) []E {
r := make([]E, 0, len(kvs))
// error: cannot range over kvs (T has no core type)
for _, v := range kvs {
r = append(r, v)
}
return r
}
func keys[T map[int]string | map[int]int](kvs T) []int {
r := make([]int, 0, len(kvs))
// error: cannot range over kvs (T has no core type)
for k := range kvs {
r = append(r, k)
}
return r
}
func sum[M map[int]int | map[string]int](m M) (sum int) {
// error: cannot range over m (M has no core type)
for _, v := range m {
sum += v
}
return
}
func foo[T []int | []string] (v T) {
// error: cannot range over v (T has no core type)
for range v {}
}
func bar[T [3]int | [6]int] (v T) {
// error: cannot range over v (T has no core type)
for range v {}
}
type MyInt int
func cat[T []int | []MyInt] (v T) {
// error: cannot range over v (T has no core type)
for range v {}
}
type Slice []int
func dot1[T []int | Slice] (v T) {
for range v {} // okay
}
func dot2[T ~[]int] (v T) {
for range v {} // okay
}
该限制是有意的。我认为其目的是确保两者 两个迭代变量始终具有指定的类型 (普通类型或类型参数类型)。 但是,对于此意图,此限制过于严格。 因为在实践中,某些容器的键类型或元素类型是相同的, 即使容器的基础类型不同。 在许多用例中,两个迭代变量中的一个被忽略。
我不确定该限制是否会在未来的 Go 版本中删除。 在我看来,这种限制在一定程度上降低了 Go 自定义泛型的实用性。
如果所有可能的类型都是切片和数组,并且它们的元素类型相同, 我们可以使用普通循环来绕过这个限制。for
func cat[T [3]int | [6]int | []int] (v T) {
for i := 0; i < len(v); i++ { // okay
_ = v[i] // okay
}
}
对预先声明的函数的调用在这里是有效的。 后面的部分将讨论这一点。
下面的代码也无法编译,但这是合理的。 因为 string 的迭代元素rune是值, 而 []byte的迭代元素是byte值。
func mud[T string | []byte] (v T) {
for range v {} // error: cannot range over v (T has no core type)
}
如果它打算循环访问任一字节切片和字符串中的字节, 我们可以使用以下代码来实现目标。
func mud[T string | []byte] (v T) {
for range []byte(v) {} // okay
}
转换(如果它遵循关键字)是 []byte(v)由官方标准 Go 编译器优化,使其不重复range 基础字节。
以下函数现在无法编译(Go 1.22), 即使两个迭代变量的类型始终是int 和 rune。 它是否会在未来的 Go 版本中编译尚不清楚。
func aka[T string | []rune](runes T) {
// cannot range over runes (T has no core type)
for i, r := range runes {
_ = i
_ = r
}
}
4 涉及转换的类型参数
首先,我们应该知道普通类型/值的转换规则。
根据当前规范(Go 1.22), 给定两个类型 ,假设其中至少一个是类型参数, 则其中值可以转换为 if 中每种类型的值另一个的类型集可以转换为类型集中的每种类型(请注意,普通类型的类型集仅包含普通类型本身)。
例如,以下所有函数都可以正常编译。
func pet[A ~int32 | ~int64, B ~float32 | ~float64](x A, y B){
x = A(y)
y = B(x)
}
func dig[From ~byte | ~rune, To ~string | ~int](x From) To {
return To(x)
}
func cov[V ~[]byte | ~[]rune](x V) string {
return string(x)
}
func voc[V ~[]byte | ~[]rune](x string) V {
return V(x)
}
但以下函数编译失败, 因为string值可能无法转换为int 。
func eve[X, Y int | string](x X) Y {
return Y(x) // error
}
以下函数不会编译,即使其中的转换 对所有可能的类型参数都有效。 原因是普通类型,而不是类型参数, 它的基础类型是它自己。
func jon[T byte](x string) []T {
return []T(x) // error
}
将来的 Go 版本可能会放宽规则,使上述示例中的转换有效。
通过使用官方标准 Go 编译器,在下面的程序中,
函数,不编译。 原因是 type 的值不能直接转换为。
其他三个泛型函数都可以编译好。 但是,该函数不应按照上述规则进行编译。 这可能是标准编译器的错误,也可能是 当前的 Go 规范需要稍作调整。
package main
type Age int
type AgePtr *Age
func dot[T ~*Age](x T) *int {
return (*int)(x) // okay
}
func tup(x AgePtr) *int {
// error: cannot convert x (variable of type AgePtr)
// to type *int
return (*int)(x)
}
func tup2(x AgePtr) *int {
return (*int)((*Age)(x))
}
func pad[T AgePtr](x T) *int {
// error: cannot convert x to type *int
return (*int)(x)
}
func pad2[T AgePtr](x T) *int {
return (*int)((*Age)(x))
}
func main() {
var x AgePtr
var _ = dot[AgePtr](x)
var _ = tup2(x)
var _ = pad2[AgePtr](x)
}
5 小结
在泛型函数体中, 仅当类型参数的值有效时,对 type 参数的值有效 对 type 参数约束的 type 集中的每种类型的值都有效。 在当前的自定义泛型设计和实现(Go 1.23)中, 反之亦然。 必须满足一些额外的要求才能使作有效。
Go 1.18 版本引入了泛型以及许多新功能,包括类型参数、类型约束和类型集等新概念。它还引入了核心类型的概念。
虽然前者提供了具体的新功能,但核心类型是一种抽象结构,是为了方便和简化对泛型作数(类型为类型参数的作数)的处理而引入的。
在 Go 编译器中,过去依赖于作数底层类型的代码现在必须调用计算作数核心类型的函数。在语言规范中,在许多地方,我们只需要将“底层类型”替换为“核心类型”。
参考
2022 年 12 月 15 日的 Go 编程语言规范版本
- 点赞
- 收藏
- 关注作者
评论(0)