泛型在go的使用建议
1 简介
本章将讨论对类型参数值的哪些操作 在泛型函数体中是有效的,哪些是无效的。
在泛型函数体中, 对类型参数值的操作仅在以下情况下有效: 对 Type 参数约束的 Type 集中的每种类型的值都有效。 在当前的自定义通用设计和实现(Go 1.22)中, 反之亦然。 必须满足一些额外的要求才能使操作有效。
目前,有许多这样的限制。其中一些是暂时的 并且可能会从未来的 Go 版本中删除,有些是永久性的。 临时的主要是实现工作量造成的, 因此,它们最终需要一些时间和努力才能被移除。 永久性的是由自定义泛型设计原则引起的。
2 对类型参数类型的值的操作
以下内容将列出这些限制。 还将列出一些事实和相关概念。
每个类型化值必须具有指定的类型,泛型函数中使用的类型是相同的
如上一章所述,从 Go 1.18 开始, Go 中的值类型可以分为两类:
- 类型参数类型:类型参数列表中声明的类型。
普通类型:类型参数列表中未声明的值类型。 在 Go 1.18 之前,只有普通类型。
Go 自定义泛型不是作为简单的代码文本模板实现的。
这是与代码生成的根本区别。 go编程中有一个原则规则: 每个类型化表达式都必须具有指定的类型, 它可以是普通类型,也可以是类型参数。
例如,在以下代码片段中,只有函数dot不编译。 其他的编译正常。 原因很简单:
在函数中,类型为 ,是类型参数。 当然,在函数的使用中,可以实例化为 或 , 但这并不能改变这样一个事实,即从编译器的角度来看, foox 其类型是类型参数。
在函数中,bar 和 x[i]x[y]E的类型都是类型参数。
在函数中,win和 x[1]x[y]int的类型都是指定的普通类型。
在函数中,dot和 x[1]x[y]intstring的类型可能是 or(两种不同的普通类型),尽管它们总是相同的。
func foo[T int | string](x T) {
var _ interface{} = x // okay
}
func bar[T []E, E any](x T, i, j int) () {
x[i] = x[j] // okay
}
func win[T ~[2]int | ~[8]int](x T, i, j int) {
x[i] = x[j] // okay
}
func dot[T [2]int | [2]string](x T, i, j int) {
x[i] = x[j] // error: invalid operation
var _ any = x[i] // error: invalid operation
}
字符串的元素类型被视为 ,因此以下代码编译:byte
func ele[ByteSeq ~string|~[]byte](x ByteSeq, n int) {
_ = x[n] // okay
}
出于同样的原因(原则规则),在以下代码片段中, 函数和两者都编译正常, 但该函数没有。nopjammud
func nop[T *Base, Base int32|int64](x T) {
*x = *x + 1 // okay
}
func jam[T int32|int64](x *T) {
*x = *x + 1 // okay
}
func mud[T *int32|*int64](x T) {
*x = *x + 1 // error: invalid operation
}
同样,在下面的代码片段中,只有函数编译失败, 另外两个都可以编译。box
func box[T chan int | chan byte](c T) {
_ = <-c // error
}
func sed[T chan E, E int | byte](c T) {
_ = <-c // okay
}
type Ch <-chan int
func cat[T chan int | Ch](c T) {
_ = <-c // okay
}
3 类型参数可以被类型断言
由于类型参数是指定的类型,因此可以断言到类型。 即使存在重复的类型表达式,也会编译以下代码 在casetype-switchwua函数内的代码块中的运行时。
import "fmt"
func nel[T int | string](x any) {
if v, ok := x.(T); ok {
fmt.Printf("x is a %T\n", v)
} else {
fmt.Printf("x is not a %T\n", v)
}
}
func wua[T int | string](x any) {
switch v := x.(type) {
case T:
fmt.Println(v)
case int:
fmt.Println("int")
case string:
fmt.Println("string")
}
}
类型参数不能用作(本地)命名常量的类型
这意味着类型参数的值都是非常量。
例如,以下函数无法编译。
func f[P int]() {
const y P = 5 // error: invalid constant type P
}
这个事实永远不会改变。
因此,将常量转换为类型参数会产生 传递给 Type 参数的参数的非常量值。 例如,在下面的代码中,该函数编译: 但该函数没有。hg
const N = 5
func g[P int]() {
const _ = P(N) // error: P(N) is not constant
}
func h[P int]() {
var _ = P(N) // okay
}
由于转换规则,两者的返回结果 函数mud和tex 是不同的。
package main
const S = "Go"
func mud() byte {
return 64 << len(string(S)) >> len(string(S))
}
func tex[T string]() byte {
return 64 << len(T(S)) >> len(T(S))
}
func main() {
println(mud()) // 64
println(tex()) // 0
}
#
func Output[T any]() {
var t T
fmt.Printf("%#v\n", t)
}
type A struct {
a,b,c,d,e,f,g int64
h,i,j string
k []string
l, m, n map[string]uint64
}
type B A
func main() {
Output[string]()
Output[int]()
Output[uint]()
Output[int64]()
Output[uint64]() // 上面每个都underlying type都不同,尽管int64和uint64大小一样,所以生成5份不同的代码
Output[*string]()
Output[*int]()
Output[*uint]()
Output[*A]() // 所有指针都是同一个shape,所以共用一份代码
Output[A]()
Output[*B]()
Output[B]() // B的underlying tyoe和A一样,所以和A共用代码
}
4小结
实现泛型有很多种方法,常见的主流的是下面这些:
以c++为代表的,类型参数就是个占位符,最后实际上会替换成实际类型,然后以此为模板生成实际的代码,生成多份代码,每份的类型都不一样
以TypeScript和Java为代表的类型擦除,把类型参数泛化成一个满足类型约束的类型(Object或者某个interface),只生成一份代码
以c#为代表,代码里表现的像类型擦除,但运行的时候实际上和c++一样采用模板实例化对每个不同的类型都生成一份代码
golang有自己的gcshape。 简单说,所有拥有相同undelyring type的类型都算同一种shape,所有的指针都算一种shape,除此之外就算两个类型大小相同甚至字段的类型相同也不算同一个shape。
使用建议
明确使用 *T,而不是让T代表指针类型
明确使用 []T和 map[T1]T2,而不是让T代表slice或map
少写泛型函数,可以多用泛型struct
类型约束的core type直接影响被约束的类型可以执行哪些操作
- 点赞
- 收藏
- 关注作者
评论(0)