泛型系列:关于Golang泛型的实现方案以及个人理解(1)

作者前言

距离上次更新公众号也有一年半的时间了,这一年半几乎没怎么学习也没怎么看书,然后导致个人非常的颓废堕落,整日不知道该做些什么,完全没有目标感,索性在女友的帮助下捡起来了很久未更新的公众号,当作对自身颓废和没有目标的一些改变,从本次开始会陆续重新开启公众号的更新,但是多久更新一次暂且不知道,学到哪,总结到哪就写到哪,希望各位看官有所理解。

关于本系列

关于Go的泛型已经大概更新了一年左右了,我由于这段时间的颓废也没怎么看过关于泛型的一些相关内容,在工作中也没有激进的直接使用泛型,但是今天看到了Go Team 关于泛型设计的一个提案(后称 提案)的设计文档(Type Parameters Proposal),目前看只有英文版,所以用自己不怎么样的英语加上翻译软件并结合一些自己的粗浅的理解,发布本系列文章与诸君共勉,因为时间关系与该文档内容过于丰富,所以拆分成一个系列来进行发布,因为能力有限,文章中可能会出现一些理解失误或者语言不通,望诸君见谅并且私信作者提出问题共同探讨,在此提前感谢诸君。

本文中代码段有些未完全搬运于原文,会根据个人理解进行自行编写,Go版本采用1.18.6

正文

当前设计文档的状态

本篇文档所阐述的类型参数的通用编程设计已被采纳到Go整体泛型设计,并且已经在今年初的1.18版本中实现并发布。

摘要

提案中表示,建议(因为是泛型的提案所以用了建议一词)在当前Go之上,在类型和函数声明的过程中增加可选的类型参数,类型参数受到接口类型的约束。当把接口类型用作于类型约束时,支持支持嵌入其他元素,但其必须满足限制的类型约束的集合。参数化(个人觉得这里叫泛型化更好理解?)的类型和函数可以在满足类型约束的前提下可以使用带有类型参数的运算符。通过统一算法进行类型推断允许在许多情况下从函数调用中省略类型参数。该设计完全向后兼容 Go 1。

太长不看系列

本节非常简要地解释了提案的更改。本节适用于已经熟悉泛型如何在 Go 等语言中工作的人。这些概念将在以下部分中详细解释。

  1. 函数可以有一个额外的类型参数列表,它使用方括号(Ps:方括号真丑),但在其他方面看起来像一个普通的参数列表:

    func ThisIsAGenericFunction[T any](value T) {
    	// todo
    }
    
  2. 这些类型参数可以由常规参数使用,也可以在函数体中使用。

  3. Types can also have a type parameter list(这句话翻译出来是“类型还可以具有类型参数列表”,但是看了半天代码没懂是啥意思,然后仔细品了一下发现是:切片/数组类型也可以具有类型参数…):

    type GenericSlice[T any] []T
    type GenericArray[T any] [5]T
    
    func main() {
    	var NewGenericSlice GenericSlice[int]
    	NewGenericSlice = append(NewGenericSlice, 1)
    	fmt.Println(NewGenericSlice)
    
    	var NewGenericArray GenericArray[int]
    	NewGenericArray[0] = 55
    	fmt.Println(NewGenericArray)
    }
    
  4. 与每个普通参数都有一个参数类型一样,每个类型参数同样的也有一个类型约束

    func ThisIsAOrdinaryFunction(value1 int, value2 int16, value3 int32) {
    	// todo
    }
    func ThisIsAGenericFunction2[T any](value1 T, value2 T, value3 T) {
    	// todo
    }
    
  5. 类型约束是接口类型:

    type GenericTypeConstraints interface {
      // | 是新加的符号,代表下列几种类型都行
    	int8 | int16 | int32 | int64
    }
    
    func GenericTypeConstraintsFunction[T GenericTypeConstraints](value T) {
    	// todo
    }
    
    func main() {
    	var value int16 = 222
      // 正确调用,显式确认类型参数类型,int16处于GenericTypeConstraints 类型约束中
    	GenericTypeConstraintsFunction[int16](value)
      // 正确调用,隐式确认类型参数类型,通过类型推倒来判断类型参数,int16处于GenericTypeConstraints 类型约束中
    	GenericTypeConstraintsFunction(value) 
    	var value2 int = 222
      // 错误调用,显式确认类型参数类型,但是int类型未在 GenericTypeConstraints 类型约束中
    	GenericTypeConstraintsFunction[int16](value2)
      // 错误调用,隐式确认类型参数类型,通过类型推倒来判断类型参数,但是int类型未在 GenericTypeConstraints 类型约束中
    	GenericTypeConstraintsFunction(value2)
    }
    
    1. 一个新的关键字any代表任何类型都可以用作类型参数:

      func ThisIsAGenericFunction[T any](value T) {
      	// todo
      }
      
      func main() {
      	ThisIsAGenericFunction[string]("可以")
      	ThisIsAGenericFunction[int](0)
      	ThisIsAGenericFunction[bool](true)
      	ThisIsAGenericFunction[[]float64]([]float64{1, 2, 4, 5})
      	ThisIsAGenericFunction[map[string]string](map[string]string{"可以": "可以"})
      }
      
    2. 可以在用于进行类型约束的接口类型中增加其他的类型以完成类型约束:

      1. T限制为该类型:

        type GenericTypeConstraints interface {
        	int
        }
        type NewInt int // 此类型不符合类型约束
        
      2. ~T限制其基础类型为T的所有类型

        type GenericTypeConstraints2 interface {
        	~int
        }
        type NewInt int // 此类型也符合类型约束
        
      3. T1 | T2 | ...限制为列出的任何元素

        type GenericTypeConstraints2 interface {
        	~int | float64 | string
        }
        
      4. 这是我自己总结的:comparable 限制类型为可比较类型(布尔型,数字,字符串,指针,通道,基本类型是可比较类型的数组,所有字段类型都是可比较类型的结构体)

        func GenericTypeConstraintsCompareFunction[T comparable](a, b T) bool {
        	return a == b
        }
        
    3. 泛型函数调用时,所传参数必须符合类型约束中所支持的类型。

    4. 使用泛型函数或类型时需要传递类型参数。

    5. 类型推断允许在常见情况下省略函数调用的类型参数(如摘要中所述)

设计背景(不太重要,机翻版)

有许多要求在 Go中添加对泛型编程的额外支持。关于问题跟踪器活文档已经进行了广泛的讨论。

这种设计建议扩展 Go 语言以添加一种形式的参数多态性,其中类型参数不受声明的子类型关系(如在某些面向对象语言中)的限制,而是受明确定义的结构约束。

此版本的设计与 2019 年 7 月 31 日提出的设计草案有许多相似之处,但合同已被删除并被接口类型所取代,并且语法也发生了变化。

已经有几个添加类型参数的建议,可以通过上面的链接找到。这里提出的许多想法以前都出现过。这里描述的主要新特性是语法和对接口类型作为约束的仔细检查。

此设计不支持模板元编程或任何其他形式的编译时编程。

由于泛型一词在 Go 社区中被广泛使用,我们将在下面使用它作为简写来表示接受类型参数的函数或类型。不要将本设计中使用的通用术语与其他语言(如 C++、C#、Java 或 Rust)中的相同术语混淆;它们有相似之处,但并不相同。

具体设计
类型参数(Type parameters)

泛型代码使用抽象数据类型编写,我们称之为类型参数。当运行泛型代码时,类型参数被类型参数替换(这写的好绕,大概就是说在泛型代码运行的时候,原本泛型函数/类型上的类型参数那部分([T any]这部分)会被实际逻辑的类型参数替换掉,比如 func Func[T any](v T)在调用时,传入 Func[string]("func"),那么之前的T就会被string所替换)。

下面的例子是一个打印切片中所有数据的函数,此函数是一个泛型函数,因此其切片的类型是未知的,这是我们为了支持泛型编程而希望允许的那种函数的一个简单示例(注:这只是个示例,下面的代码是无法编译通过的)。

func GenericTypePrintSlice(slice []T) { 
	for index, value := range slice {
		fmt.Println(index, value)
	}
}

使用这种方法,首先要做的决定是:应该声明如何T的类型参数?在像 Go 这样的语言中,我们希望每个标识符都以某种方式声明。

在这里,我们做出一个设计决定:类型参数应该和普通的函数参数类似,所以应与其他参数一起列出。但是实际上类型参数与普通参数不同,所以尽管他们都出现在函数的参数列表中,但是我们仍然希望使用某种方式来区分它们。

这里我们做一个设计决策:类型参数和普通的非类型函数参数类似,因此应该和其他参数一起列出。但是,类型参数与非类型参数不同,因此尽管它们出现在参数列表中,但我们希望区分它们。上述问题让我们引出了下一个设计决定:我们定义了额外的可选参数列表来描述类型参数以便于解决上述问题。

类型参数的描述列表出现在常规参数之前(函数名之后),为了区分类型参数列表和常规参数列表,类型参数列表使用方括号([])(PS:用<>能死系列)而不是圆括号(())。与常规参数拥有具体类型一样,类型参数有一个属于自己的元类型,我们称之为约束。具体细节将在后续讨论,我们只需要针对之前的代码进行一下简单的修改就可以让之前无法编译通过的函数能正常的完成其打印任意类型切片的功能。

func GenericTypePrintSlice[T any](slice []T) {
	for index, value := range slice {
		fmt.Println(index, value)
	}
}

这表示在函数GenericTypePrintSlice中,标识符T是一种当前未知但在调用函数时将知道的类型,即类型参数any可以表示T是任何的类型。综上所述,在描述普通参数时,类型参数(T)可以用作该参数的类型标识,同时它也可以在函数体中当作类型标识使用。

func GenericTypeDeepCopySlice[T any](slice []T) []T {
	NewSlice := make([]T, len(slice))
	copy(NewSlice, slice)
	return NewSlice
}

与普通参数列表不同的是,在类型参数列表中,类型参数需要名称以避免语法上的歧义,所以,我们并没有理由省略类型参数的名称。

由于GenericTypePrintSlice具有类型参数,因此任何对GenericTypePrintSlice的调用都必须显式提供类型参数(稍后,我们将看到通常如何从非类型参数推导出此类型参数),至少在目前我们需要显式的传递类型参数,传递类型参数的方式和声明它的方式很像:将类型参数作为单独的参数列表,通过方括号([])传递他。

GenericTypePrintSlice[string]([]string{"1", "2", "3"})
// 打印结果
// 0 1
// 1 2
// 2 3
约束(Constraints)

让我们来学习一个复杂一点的示例,我们写一个函数,让传入切片的每一个元素都使用Stringer接口的String()函数转换为字符串,并把新的字符串切片返回回来。

func GenericTypeConvertToString[T any](slice []T) []string {
	result := make([]string, 0, len(slice))
	for _, value := range slice {
		result = append(result, value.String()) // bad call 
	}
	return result
}

看起来好像没什么问题,但是在上面的函数中,value的类型为T,并且T可以是任意类型,这意味着T不需要实现Stringer接口的String()方法。因此,对value.String()的调用无效。

当然,同样的问题也会出现在支持泛型编程的其他语言中,例如,在C++中,泛型函数(在C++术语中是函数模板)可以对泛型类型的值调用任何方法。也就是说,在C++中,调用value.String()就可以了,但是如果value没有String()方法时,

使用没有String()方法的类型参数调用函数,则在在编译调用value.String()的类型参数时,会报告该错误。通常,这些报错信息会在错误发生之前可能多层的泛型函数调用,所以可能会导致这些错误很长,因为编译器必须报告整个调用堆栈才能让程序员了解到到底是哪里出现了问题。

当然对于Go来说C++的做法显然不是一个好的解决办法,原因之一是因为不同的语言风格,在Go中,我们并不引用其名称,比如在这个例子中的String(),我们希望他是存在的,当Go发现他们的时候,会将所有名称解析(编译?)为其声明。

另一个原因是Go的设计是为了支持规模化编程。我们必须考虑泛型函数的定义(如上面的Stringer接口)与对其调用(可能处于不同的package中)相隔很远的情况。那么通常,所有泛型代码都希望类型参数满足某些条件才可以使用,我们将这些条件称之为约束(Constraints)(注:其他语言也有类似的概念,其称为类型边界或特征边界)。那么在上面的示例下,我们需要的约束就很明显,传入参数的类型必须实现Stringer接口的String()string方法,在其他情况下,它可能没有那么明显(作者也没太理解这个其他情况下指的是什么)。

我们并不希望从Stringer碰巧要做的事情(比如上面这个示例的调用String()方法)中派生出约束。如果我们这样做会导致后续针对Stringer接口的任何微小的更改(当然这是官方接口改的概率应该不大,只是拿他举例子),都会导致在某些莫名其妙调用该函数的地方出现意外崩溃或者中断。如果说当Stringer更改的时候,我们随之进行修改,虽然这也是可接受的,但是我们仍然需要避免因为Stringer的更改导致整个程序的函数调用出现莫名其妙的中断或崩溃。

从上述问题中就得到了一个设计理念,约束必须要对调用方传递的类型参数和泛型函数中的代码都进行限制。一是对调用方所传递的类型参数必须满足约束条件,泛型函数只能使用满足了约束条件的传入值,这也就产生了Go泛型编程中的一条重要的规则:

泛型代码只能使用其类型参数已知的操作来实现。

作者理解:传入类型必须符合约束条件才可正确使用

any类型所允许的操作(Operations permitted for any type)

在我们进一步讨论约束之前,让我们简单的留意一下当把约束设定为any时,会出现什么问题。当泛型函数的类型参数使用any约束时,就会与最开始的[类型参数](#类型参数(Type parameters))中的GenericTypePrintSlice方法一样,允许传入任何类型的切片,泛型函数可以对该类型参数的值使用的唯一操作就是那些对任何类型的值都允许的操作。在上面的示例中,GenericTypePrintSlice函数声明一个变量value,其类型为类型参数T,并将该变量传递给函数。

那么什么是:对任何类型的值都允许的操作 ?具体操作方法如下:

  • 声明这些类型的变量
  • 将相同类型的值赋值给这些变量
  • 将这些变量传递给函数或从函数返回它们
  • 获取这些变量的地址
  • 将这些类型的值转换或分配给类型interface{}
  • 将类型的值转换T为类型T(允许但无用)
  • 使用类型断言将接口值转换为T类型
  • switch type中使用类型作为某个case的类型
  • 定义和使用这些类型的复合类型,例如该类型的切片
  • 将类型传递给一些预先声明的函数,例如new

未来的语言更改可能会添加其他此类操作,尽管目前预计不会。

定义约束

就目前而言,Go 已经有一个接近于我们需要的约束的构造方式:接口类型。接口类型是一组方法的集合。只有同样实现了相同接口方法的那些值才能赋值给接口类型的变量。除了允许任何类型的操作之外,可以使用接口类型的值执行的唯一操作是调用方法。

使用类型参数调用泛型函数类似于一个接口类型的变量赋值:类型参数必须实现(符合)类型参数的约束

编写泛型函数类似于使用接口类型的值:泛型代码只能使用约束允许的操作(或任何类型允许的操作)。

因此,在这个设计中,约束只是接口类型。满足约束意味着实现接口类型。

any约束

既然我们知道约束只是接口类型,我们就可以解释any作为约束的含义。如上所示,any约束允许任何类型作为类型参数,并且只允许函数使用允许任何类型的操作。其接口类型是空接口:interface{} (实际上在源代码中 any的定义为:type any = interface{},其实就是个interface{}的别名)。因此我们也可以把GenericTypePrintSlice改成这样:

func GenericTypePrintSlice[T interface{}](slice []T) {
	for index, value := range slice {
		fmt.Println(index, value)
	}
}

然而,每次编写不约束类型参数的泛型函数时,都必须要写上interface{},这是很没意思的,所以在这个设计中,我们建议了一个类型约束any,它等价于interface{}(实际上在源代码中 any的定义为:type any = interface{},其实就是个interface{}的别名)。这将是一个预声明的名称(关键字),在语义块中隐式声明。除在泛型函数中用作类型约束外,将any用作任何其他地方都是无效的。

显然,我们可以将any作为interface{}的别名或定义为interface{}的新定义类型来使用。然而我们不希望这种与泛型相关的设计会导致原有的非泛型相关代码发生重大改变。将any添加为interface{}的通用名称可以而且应该单独讨论).

使用约束

约束对于泛型函数而言,可以视作类型参数的类型,我们称之为:元类型。如下所示,约束作为类型参数的元类型出现在类型参数列表中。

func Stringify[T fmt.Stringer](s []T) (ret []string) {
	for _, v := range s {
		ret = append(ret, v.String())
	}
	return ret
}

单个类型参数T后面跟着对T的约束,在本例中为fmt.Stringer

多种类型参数

尽管该Stringify示例仅使用单个类型参数,但函数可能具有多个类型参数。

func GenericTypeMultiplePrint[T1, T2 any](v1 []T1, v2 []T2) {...}
func GenericTypeMultiplePrint2[T any](v1 []T, v2 []T) {...}

这两个实例函数的区别在于,GenericTypeMultiplePrint中参数v1v2可以是不同类型的切片,但是GenericTypeMultiplePrint2中的参数v1v2必须是相同类型的切片。

正如每个普通参数可能有自己的类型一样,每个类型参数也可能有自己的约束。

type Plusser interface {
	Plus(string) string
}

func ConcatTo[S fmt.Stringer, P Plusser](s []S, p []P) []string {
	r := make([]string, len(s))
	for i, v := range s {
		r[i] = p[i].Plus(v.String())
	}
	return r
}

单个约束可以用于多个类型参数,就像单个类型可以用于多个非类型函数参数一样。该约束分别适用于每个类型参数。

func Stringify2[T1, T2 fmt.Stringer](s1 []T1, s2 []T2) string { 
	r := "" 
	for _, v1 := range s1 { 
		r += v1.String() 
	} 
	for _, v2 :=range s2 { 
		r += v2.String() 
	}
	return r 
}
泛型类型(Generic types)

我们想要的不仅仅是泛型函数,还需要泛型类型。提案中建议对类型进行扩展,使其能够接受类型参数。

type GenericType[T any] []T

类型的类型参数就像函数的类型参数一样。

在类型定义中,类型参数可以像其他类型一样使用。

若要使用泛型类型,必须提供类型参数。这称为实例化。与泛型函数一样,类型参数出现在方括号([])中。

当我们通过为类型参数提供类型参数来实例化一个类型时,我们会产生一个类型,其中类型定义中对类型参数的每一次使用都被相应的类型参数所取代。

作者注:有点绕,其实就是说当为一个泛型类型提供类型参数来实例化他的时候,就会产生一个新的类型,这个泛型类型的时候在每次使用时都会被这个新类型所替代

type GenericType[T any] []T
// V是int的GenericType。
// 这类似于假装 type "GenericType[int]" []int 是一个有效的标识符
// 并且产生
// type GenericType[int] []int 
// var V GenericType[int]
// 对所有使用 GenericType[int] 的地方都将引用相同的 “GenericType[int]” 类型。
// 有点绕 还是看上面引用的解析吧
func main() {
	var V GenericType[int]
}

与普通的类型创建一样,泛型类型也可以有方法的存在,与普通方法不同的是,泛型类型方法中接收器的类型必须要在声明中与接收器类型定义中声明的类型参数数量相同。除此之外,它们的声明没有任何限制。如案例:

type GenericType[T any] []T
// 接收器类型为指针
// 可以在参数列表中使用T
func (receiver *GenericType[T]) Append(value T) {
	*receiver = append(*receiver, value)
}
// 接收器类型为值
func (receiver GenericType[T]) Print() {
	for i, t := range receiver {
		fmt.Println(i, t)
	}
}
// 这个接收器的类型参数多了。所以是一个无效的方法
func (receiver GenericType[T1, T2]) INVALID() {
	// fixme
}

func (receiver GenericType[_]) IgnoreTypeParam()  {
	// 我忽略了类型参数
}

func main() {
	var Value = GenericType[int]{1, 2, 3, 4, 6, 24, 12, 31, 425, 413, 45, 41}
	Value.Print()
	Value.Append(123)
	Value.Print()
}

方法声明中列出的类型参数不必与类型声明中的类型参数具有相同的名称,如果你不想使用这个类型参数做什么的话,你可以像忽略返回值一样使用_忽略掉他,眼不见为净嘛。

泛型类型可以引用自身,但是当他需要引用自身的时候,类型参数的顺序必须相同。因为此限制可防止类型实例化的无限递归。如案例:

// Node 这是个可以放任意类型的链表
type Node[T any] struct {
	Next *Node[T] // 引用自身
	Val  T
}
// 这个类型是无效的。
type InvalidNode[T1, T2 any] struct {
	Next *InvalidNode[T2, T1] // 引用自身
}

作者注:上面的案例 第二个其实反过来搞也没有报错并且可以运行…但是从逻辑上讲还是不要改变顺序,正如官方所言

类型参数的顺序必须相同。因为此限制可防止类型实例化的无限递归

如果不按顺序的话,这种逻辑可能把自己写懵了

此限制适用于直接和间接引用。

// ListHead是链表的头。
type ListHead[T any] struct {
	head *ListElement[T]
}

// ListElement是链表中带头部的元素。
// 每个元素都指向头部。
type ListElement[T any] struct {
	next *ListElement[T]
	val  T
	// 在这里使用ListHead[T]是可以的。
	// ListHead[T]引用 ListElement[T] 引用ListHead[T]。
	// 不能使用ListHead[int],因为ListHead[T]将间接引用ListHead[int]。
	head *ListHead[T]
}

上面的作者注所说的问题,设计者也表明了立场。看起来是已经将这个规则放宽了。

注意:随着人们对如何编写代码有了更多的了解,可以放宽这一规则,允许某些情况下使用不同类型的参数。

Note: with more understanding of how people want to write code, it may be possible to relax this rule to permit some cases that use different type arguments.

和泛型函数相同,泛型类型的类型参数也可以有除 any以外的约束。

// StringableVector 是某种类型的切片,其中类型必须实现了Stringer接口
type StringableVector[T Stringer] []T

func (s StringableVector[T]) String() string {
	var sb strings.Builder
	for i, v := range s {
		if i > 0 {
			sb.WriteString(", ")
		}
		// 这里可以调用v.String,因为v是T类型的,而T的约束是Stringer。
		sb.WriteString(v.String())
	}
	return sb.String()
}
方法不可以接受额外的类型参数

尽管泛型类型的方法可以使用该类型的参数,但方法本身不能具有其他类型参数。在需要向方法添加类型参数的地方,人们将不得不编写一个适当的类型参数化的顶层函数。

总结

因为时间关系与该文档内容过于丰富,所以当前文章先终止于此,望诸君见谅,后续文章正在发布中,敬请期待。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值