golang 泛型初探
基本语法#
如果我想编写一个可以输出任何给定类型的切片并且不使用反射的打印功能,则可以使用新的泛型语法。
func print[T any](slice []T) {
fmt.Print("Generic: ")
for _, v := range slice {
fmt.Print(v, " ")
}
fmt.Print("\n")
}
这是单个打印功能的实现,可以使用新的泛型语法输出任何给定类型的切片。这种语法的好处是,函数内部的代码可以使用可与具体类型一起使用的语法和内置函数。当我使用空接口编写通用代码时,情况并非如此。
我需要一种方法告诉编译器,我不会显式声明 T 类型,但必须由编译器在编译时确定。新语法为此使用方括号。括号定义了通用类型标识符的列表,这些通用类型标识符表示在编译时确定的特定于函数的类型。这样告诉编译器,在程序编译之前不会声明具有这些名称的类型。这些类型需要在编译时弄清楚。
注意:尽管当前示例仅使用一个,但可以在方括号内定义多个类型标识符,例如: [T, S, R any]
可以使用任何帮助你想提高代码可读性的名称来命名这些类型标识符。在这种情况下,可以使用大写字母 T 来描述将传递某种类型 T(在编译时确定)的切片。、
括号内也使用了 any 一词。这代表了对 T 类型的限制。编译器要求所有通用类型都具有定义明确的约束。 any 约束由编译器预先声明,并声明对 T 类型可以没有约束。
numbers := []int{1, 2, 3}
print[int](numbers)
strings := []string{"A", "B", "C"}
print[string](strings)
floats := []float64{1.7, 2.2, 3.14}
print[float64](floats)
这是调用通用打印功能的方法,其中在调用上显式提供了 T 的类型信息。 语法模仿了这样的想法,即函数声明 func name [T any](slice [] T){定义了两组参数。 第一组是映射到相应类型标识符的类型,第二组是映射到相应输入变量的数据。
编译器可以推断类型,而无需在调用站点上显式传递类型信息。
numbers := []int{1, 2, 3}
print(numbers)
strings := []string{"A", "B", "C"}
print(strings)
floats := []float64{1.7, 2.2, 3.14}
print(floats)
这段代码说明了如何无需显式传递类型信息即可调用通用打印函数。 在函数调用时,编译器能够识别用于 T 的类型,并构造函数的具体版本以支持该类型的切片。 编译器能从传入的数据中推断出其在调用站点上拥有的信息的类型。
底层类型#
如果我想使用基础类型声明自己的泛型类型怎么办?
type vector[T any] []T
func (v vector[T]) last() (T, error) {
var zero T
if len(v) == 0 {
return zero, errors.New("empty")
}
return v[len(v)-1], nil
}
此示例显示了通用向量类型,该向量类型将向量的构造限制为单个数据类型。 使用方括号声明类型 T 是要在编译时确定的通用类型。 任何约束的使用都描述了 T 可以成为哪种类型没有约束。
最后一个方法用类型为 vector [T] 的值接收器声明,以表示带有类型 T 的基础切片的类型为 vector 的值。该方法返回相同类型 T 的值。
func main() {
fmt.Print("vector[int] : ")
vGenInt := vector{10, -1}
i, err = vGenInt.last()
if i < 0 {
fmt.Print("negative integer: ")
}
fmt.Printf("value: %d error: %v\n", i, err)
fmt.Print("vector[string] : ")
vGenStr := vector{"A", "B", string([]byte{0xff})}
s, err = vGenStr.last()
if !utf8.ValidString(s) {
fmt.Print("non-valid string: ")
}
fmt.Printf("value: %q error: %v\n", s, err)
}
Output:
vector[int] : negative integer: value: -1 error: <nil>
vector[string] : non-valid string: value: "\xff" error: <nil>
当你在构造时在向量中设置值时,这就是如何使用 int 的基础类型构造向量类型的值的方法。 该代码的一个重要方面是构造调用。
// Zero Value Construction
var vGenInt vector[int]
var vGenStr vector[string]
// Non-Zero Value Construction
vGenInt := vector{10, -1}
vGenStr := vector{"A", "B", string([]byte{0xff})}
在将这些泛型类型构造为零值时,编译器无法推断出该类型。 但是,在构造过程中进行初始化的情况下,编译器可以推断类型。
规范的一个方面着重于将通用类型构造为零值状态。
type vector[T any] []T
func (v vector[T]) last() (T, error) {
var zero T
if len(v) == 0 {
return zero, errors.New("empty")
}
return v[len(v)-1], nil
}
需要关注最后的方法声明以及该方法如何返回通用类型 T 的值。在第一次返回时,需要返回类型 T 的零值。当前的草案提供了两种解决方案来编写此代码。 第一个解决方案是名为零的变量被构造为类型 T 的零值状态,然后将该变量用于返回。另一种选择是使用内置函数 new 并在 return 语句中引用返回的指针。
type vector[T any] []T
func (v vector[T]) last() (T, error) {
if len(v) == 0 {
return *new(T), errors.New("empty")
}
return v[len(v)-1], nil
}
此版本的 last 使用内置函数 new 进行零值构造,并对返回的指针进行解引用以满足返回类型 T。
注意:我可能认为为什么不使用 T {}
进行零值构造? 问题在于此语法不适用于所有类型,例如标量类型(int,string,bool), 所以这不是一个选择。
结构类型#
如果我想使用结构类型声明自己的泛型类型怎么办?
type node[T any] struct {
Data T
next *node[T]
prev *node[T]
}
声明此 struct 类型代表链表的节点。 每个节点包含一个单独的数据,由列表存储管理。 使用方括号声明类型 T 是要在编译时确定的通用类型。 任何约束的使用都描述了 T 可以成为哪种类型没有约束。
声明了类型 T 后,现以将 Data 字段定义为某种类型 T 的字段,稍后再确定。 next 和 prev 字段需要指向相同类型 T 的节点。这些分别是指向链表中的下一个和上一个节点的指针。 为了进行这种连接,将这些字段声明为指向使用方括号绑定到类型 T 的节点的指针。
type list[T any] struct {
first *node[T]
last *node[T]
}
第二种结构类型称为 list,它通过指向列表中的第一个和最后一个节点来表示节点的集合。 这些字段需要指向某种类型 T 的节点,就像该节点类型的 next 和 prev 字段一样。
标识符 T 再次定义为通用类型(稍后将确定),该通用类型可以替代任何具体类型。 然后,使用方括号语法将第一个字段和最后一个字段声明为指向某个类型 T 的节点的指针。
func (l *list[T]) add(data T) *node[T] {
n := node[T]{
Data: data,
prev: l.last,
}
if l.first == nil {
l.first = &n
l.last = &n
return &n
}
l.last.next = &n
l.last = &n
return &n
}
这是名为 add 的列表类型方法的实现。由于方法是通过接收者绑定到列表的,因此不需要正式的泛型类型列表声明(与函数一样)。 add 方法的接收者被声明为指向 T 类型列表的指针,而返回则被声明为指向相同 T 类型节点的指针。
构造节点之后的代码将始终是相同的,而不管列表中存储的是哪种数据类型,因为那仅仅是指针操作。这只是受要管理的数据类型影响的新节点的构造。多亏了泛型,节点的构造可以绑定到类型 T,该类型 T 在以后的编译时被替换。
没有泛型,由于需要在编译之前将节点的构造硬编码为已知的声明类型,因此整个方法将需要重复。由于针对不同数据类型需要更改的代码量(对于整个列表实现)非常小,因此能够声明节点和列表来管理某种类型 T 的数据可减少代码重复和维护的成本。
type user struct {
name string
}
func main() {
// Store values of type user into the list.
var lv list[user]
n1 := lv.add(user{"bill"})
n2 := lv.add(user{"ale"})
fmt.Println(n1.Data, n2.Data)
// Store pointers of type user into the list.
var lp list[*user]
n3 := lp.add(&user{"bill"})
n4 := lp.add(&user{"ale"})
fmt.Println(n3.Data, n4.Data)
}
Output:
{bill} {ale}
&{bill} &{ale}
这是一个比较小的应用程序。 声明类型名称为 user 的用户,然后将列表构造为其零值状态以管理类型为 user 的值。 然后将第二个列表构造为其零值状态,此列表管理指向类型为 user 的值的指针。 这两个列表之间的唯一区别是,一个列表管理用户类型的值,另一个管理用户类型的指针。
由于在构造列表期间显式指定了类型为 user 的用户,因此 add 方法依次接受类型为 user 的值。 由于在构造列表期间显式指定了类型为 user 的指针,因此 add 方法接受类型为 user 的指针。
我可以在程序的输出中看到,各个列表中节点的 Data 字段与构造中使用的数据语义相匹配。
本作品采用《CC 协议》,转载必须注明作者和本文链接
推荐文章: