Go1.18新特性(二)— 范型

2022年4月9日 295点热度 0人点赞 0条评论

基本语法

定义可接受类型列表(constraint)

简单来说,Go的泛型就是将参数类型定义为一个可接受类型的列表(称为constraint),直接上例子。

例1: 方法定义上使用范型

func Max[T int|int32|int64|float32|float64] (a, b T) T {  if a > b {    return a  }  return b}

例2: 结构类型定义上使用范型

type Node[T any] struct{  Value T    Next *Node[T]}
func (n *Node[T]) Append(v T) *Node[T]{ next := &Node[T]{v,nil} n.Next = next return next}

以上例子中使用了一个预置的constraint “any”,这里any就相当于interface{},表示任何类型。

另一个预置的constraint是comparable,代表可以支持==和!=比较的类型。

让列表更简单

简化衍生类型的声明 

上面范型函数只能接受constraint列表中的类型作为调用的参数,包括这些类型的别名。但对于列表中类型的衍生类型就不能接受了。

例如:

type MyInt int64func TestMax(t *testing.T) {  var a,b MyInt = 2,3  Max(a,b)}

编译以上程序会引发编译错误,在Go中为了简化扩展类型的定义,我们可以通过"~"来表示支持类型扩展类型。利用这种方式修改方法定义即可修复上面的编译问题。

func Max[T int|int32|~int64|float32|float64] (a, b T) T {  if a > b {    return a  }    return b}


定义constraint类型

虽然有了上面的对于扩展类型的简化定义方式,但还是会很容易产生很长的contraint类型列表,而且这个列表往往会需要在很多方法定义中重复。

为了简化和复用复杂的类型列表(constraint)声明,在Go1.18中可以通过interface来定义一个constraint类型。


1 interface中定义constraint类型支持的方法

这种方式下方法所接受的类型必须要实现了该接口定义,即实现了该接口中包含的方法。

type Addable interface {  Add(a Addable) Addable}
type type1 struct { value int64}
func (t type1) Add(a Addable) Addable { b,_:=a.(type1) return type1{t.value + b.value}}
type type2 struct { value float64}
func (t type2) Add(a Addable) Addable { b,_:=a.(type2) return type2{t.value + b.value}}
func AddGen[T Addable](a T, b T) Addable {  return a.Add(b) }

这里你可能会有一个问题,那么既然有使用了 interface,那完全可以使用多态--以接口Addable作为方法的参数,定义如下:

func AddPoly(a Addable, b Addable) Addable {  return a.Add(b)}

问题来了,这里的这个AddPoly和上面使用范型定义的AddGen一样吗?

答案是不一样的,

AddGen的传入参数(a,b) 必须是同一类型,而不仅仅是都实现了Addable接口,上列中要么都是type1,要么就都是type2。

AddPoly方法就不同了,传入参数只要是实现了Addable就行,即(a,b)可以一个是type1,另一个是type2。

2 interface中直接包含可接受类型列表

这里就比较直接可以在一个interface中定义可接受类型的列表

type SupportTypes interface{  int | int32 | ~int64 | float32 | float64}
func Max[T SupportTypes] (a, b T) T { if a > b { return a } return b}

这个constraint interface可以在不同的地方被复用,Google在"golang.org/x/exp/constraints"包中也贴心的提供了很多的常用的constraint interface。

关于性能

Go的泛型实现影响性能吗?这是很多程序员最关心的问题。我们直接通过Benchmark程序来看结果。

1 constraint为基础计算类型

type SupportTypes interface{  int | int32 | ~int64 | float32 | float64}
func Max[T int | int32 | ~int64 | float32 | float64] (a, b T) T { if a > b { return a } return b}
func MaxI[T SupportTypes] (a, b T) T { if a > b { return a } return b}
func MaxInt(a, b int64) int64 { if a > b { return a } return b}
// Benchmarkfunc BenchmarkMaxInt(b *testing.B) { a := rand.Int63n(1000) c := rand.Int63n(1000) b.ResetTimer() for i:=0;i<b.N;i++{ _ = MaxInt(a,c) }}
func BenchmarkMaxGen(b *testing.B) { a := rand.Int63n(1000) c := rand.Int63n(1000) b.ResetTimer() for i:=0;i<b.N;i++{ _ = Max(a,c) }}
func BenchmarkMaxIGen(b *testing.B) { a := rand.Int63n(1000) c := rand.Int63n(1000) b.ResetTimer() for i:=0;i<b.N;i++{ _ = MaxI(a,c) }}
cpu: Intel(R) Core(TM) i5-3317U CPU @ 1.70GHzBenchmarkMaxIntBenchmarkMaxInt-4      1000000000           0.4331 ns/opBenchmarkMaxGenBenchmarkMaxGen-4      1000000000           0.4276 ns/opBenchmarkMaxIGenBenchmarkMaxIGen-4     1000000000           0.4353 ns/op

可以看出Go泛型对于基本类型的影响几乎没有。

2 constraint 为接口定义的类型(这里指类型需要实现接口中的方法)

type Addable interface {  Add(a Addable) Addable}
type type1 struct { value int64}
func (t type1) Add(a Addable) Addable { b,_:=a.(type1) return type1{t.value + b.value}}
type type2 struct { value float64}
func (t type2) Add(a Addable) Addable { b,_:=a.(type2) return type2{t.value + b.value}}
func AddPoly(a Addable, b Addable) Addable { return a.Add(b)}
func AddGen[T Addable](a T, b T) Addable { return a.Add(b)
}
var a type1 = type1{1}var c type1 = type1{1}
func BenchmarkAddPoly(b *testing.B) { b.ResetTimer() for i:=0;i<b.N;i++{    AddPoly(a,c) }}
func BenchmarkAddGen(b *testing.B) { b.ResetTimer() for i:=0;i<b.N;i++{ AddGen(a,c) }}
cpu: Intel(R) Core(TM) i5-3317U CPU @ 1.70GHzBenchmarkAddPolyBenchmarkAddPoly-4        146483520           8.360 ns/opBenchmarkAddGenBenchmarkAddGen-4     100000000          11.83 ns/op

这种情况下范型的性能明显要差于原有的多态实现。所以,类似情况最好不要使用范型来替代多态,不仅没有简化程序,反而对性能有一定影响。

最后,写给Java程序员“关于类型擦除”

Java程序员都知道,Java的泛型实现是编译时期的,在运行时是被擦除的,例如:

// Example generic classpublic class ArrayList<E extends Number> {    // ...};
ArrayList<Integer> li = new ArrayList<Integer>();ArrayList<Float> lf = new ArrayList<Float>();if (li.getClass() == lf.getClass()) { // 这里是true System.out.println("Equal");}

由于类型擦除的缘故,li.getClass() == lf.getClass() 的结果是true。

而Go的泛型则不是通过这种方式实现的,Go采用了一种不完全的单态化(monomorphization)称之为“GCShape stenciling with Dictionaries” 。这种实现方式也导致了我们在前面看到的性能测试的结果,这里我就不展开了,未来在别的文章中和大家讨论。

type Numeric interface {    int | int64 | uint | float32 | float64}
type ArrayList[T Numeric] struct {}
var li ArrayList[int64]var lf ArrayList[float64]if reflect.TypeOf(li) == reflect.TypeOf(lf) { // 这里是false fmt.Printf("they're equal") return}

由于没有类型擦除,reflect.TypeOf(li) == reflect.TypeOf(lf)结果返回为false。


72900Go1.18新特性(二)— 范型

这个人很懒,什么都没留下

文章评论