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 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!