泛型第03部分:结构类型和数据语义

介绍

在上一篇文章中,我向您展示了如何基于底层类型声明用户定义的类型。我通过使用具体类型、空接口和最后的泛型编写同一类型的不同版本的过程来做到这一点。我还提供了有关编译器在零值构造期间推断泛型类型替换的能力如何受到限制的信息,但它可以在初始化构造中进行。

在这篇文章中,我将分享一个示例,说明如何基于具有通用字段的结构声明用户定义类型。我还将讨论使用值或指针类型声明将如何改变语义。这篇文章的代码可以在这个playground链接中找到。

具体例子

如果你现在想在 Go 中编写一个链表,你必须为你想要管理的每种新数据类型编写完整的链表实现。使用新的泛型语法,您现在可以只有一个实现。

清单 1

14 type node[T any] struct {
15     Data T
16     next *node[T]
17     prev *node[T]
18 }

在清单 1 中,声明了一个结构类型,它表示链表的一个节点。每个都node包含由列表存储和管理的单个数据。标识符T被定义为泛型类型(稍后确定),这要归功于第 14 行附加到类型名称any的方括号。在相同的方括号内使用预先声明的标识符(any类型约束)告诉编译器没有约束意味着T可以成为任何类型。它指出T可以替代“any”具体类型。

注意:泛型类型声明需要一个约束作为语法的一部分。

T声明类型后,第15 行的Data字段现在可以定义为T类型的字段。第16 行和第 17 行声明的nextprev字段需要指向node[T]相同类型的 指针。这些分别是指向链表中下一个和上一个节点的指针。为了建立这种联系,字段被声明为指向通过使用方括号node绑定类型T的指针。

清单 2

20 type list[T any] struct {
21     first *node[T]
22     last  *node[T]
23 }

清单 2 显示了第二个名为list的结构类型,它通过指向列表中的第一个和最后一个节点来表示节点集合。这些字段需要指向node[T],就像清单 1 中的nextprev字段一样。

在第 20 行,标识符T被定义为可以替代“any”具体类型的通用类型(稍后确定)。然后在第 21 和 22 行,firstlast字段被声明为使用方括号语法指向node某种类型T的指针。

清单 3

25 func (l *list[T]) add(data T) *node[T] {
26     n := node[T]{
27         Data: data,
28         prev: l.last,
29     }
30     if l.first == nil {
31         l.first = &n
32         l.last = &n
33         return &n
34     }
35     l.last.next = &n
36     l.last = &n
37     return &n
38 }

清单 3 显示了add为该list类型命名的方法的实现。不需要正式的泛型类型列表声明(与函数一样),因为方法绑定到list通过接收器。该add方法的接收器被声明为一个list[T]指针类型,并返回被声明为一个指向一个node[T]的指针。

第 30 行到第 37 行的代码将始终相同,无论列表中存储的是什么类型的数据,因为这只是指针操作。只有第26 行的新构造node会受到将要管理的数据类型的影响。由于泛型,node节点的构造可以绑定到T随后在编译时被替换。

如果没有泛型,则需要复制整个方法,因为第 26 行需要在编译之前硬编码为已知的声明类型。由于需要针对不同数据类型更改的代码量(对于整个列表实现)非常少,因此能够声明nodelist管理某种类型的数据T可以降低代码重复和维护的成本。

应用

声明了nodelist类型后,我现在可以编写一个小型应用程序,该应用程序构造两个列表,在其中添加和显示数据。

清单 4

44 type user struct {
45     name string
46 }

50 func main() {
51
52     // Store values of type user into the list.
53     var lv list[user]
54     n1 := lv.add(user{"bill"})
55     n2 := lv.add(user{"ale"})
56     fmt.Println(n1.Data, n2.Data)
57
58     // Store pointers of type user into the list.
59     var lp list[*user]
60     n3 := lp.add(&user{"bill"})
61     n4 := lp.add(&user{"ale"})
62     fmt.Println(n3.Data, n4.Data)
63 }

Output
{bill} {ale}
&{bill} &{ale}

清单 4 显示了这个小应用程序。在第 44 行,声明了一个user类型名称,然后在第 53 行,定义了变量lv将构造list[user]的零值状态以管理类型user 的值。在第 59 行,定义了变量lp是第二个被构造的零值状态,这个列表管理指向类型user值的指针。这两个列表之间的唯一区别是一个管理类型user的值,另一个管理类型user的指针。

由于在第 53 行user的构造过程中明确指定了类型list,因此该add方法依次接受user第 54 行和第 55 行的类型值。由于在第 59 行user的构造过程中明确指定了类型指针,因此list在第 59行add调用该方法60 和 61 接受类型的指针user

您可以在程序的输出中看到,Data各个列表中节点的字段与构造中使用的数据语义相匹配。

强制指针语义

如果我选择更改代码并将Data字段声明为类型指针T并且add方法也接受类型指针,会发生什么情况T

playground链接

清单 5

14 type node[T any] struct {
15     Data *T                                <- CHANGED CODE
16     next *node[T]
17     prev *node[T]
18 }

25 func (l *list[T]) add(data *T) *node[T] {  <- CHANGED CODE
26     n := node[T]{
27         Data: data,
28         prev: l.last,
29     }

在清单 5 中,我更改了第 15 行和第 25 行的代码,以将Data字段声明为类型指针Tadd接受类型指针的方法T

当我构建程序时会发生什么?

清单 6

50 func main() {
51
52     // Store values of type user into the list.
53     var lv list[user]
54     n1 := lv.add(user{"bill"})     <- NOW MUST PASS A POINTER OF TYPE USER
55     n2 := lv.add(user{"ale"})      <- NOW MUST PASS A POINTER OF TYPE USER
56     fmt.Println(n1.Data, n2.Data)
57
58     // Store pointers of type user into the list.
59     var lp list[*user]
60     n3 := lp.add(&user{"bill"})    <- NOW MUST PASS A POINTER TO A POINTER OF TYPE USER
61     n4 := lp.add(&user{"ale"})     <- NOW MUST PASS A POINTER TO A POINTER OF TYPE USER
62     fmt.Println(n3.Data, n4.Data)
63 }

Output
type checking failed for main
prog.go2:54:15: cannot use (user literal) (value of type user) as *user value in argument
prog.go2:55:15: cannot use (user literal) (value of type user) as *user value in argument
prog.go2:60:15: cannot use &(user literal) (value of type *user) as **user value in argument
prog.go2:61:15: cannot use &(user literal) (value of type *user) as **user value in argument

清单 6 显示了这个问题。您可以看到编译器希望代码user在第 54 行和第 55 行传递一个类型指针,并user在第 60 和 61 行传递一个指向类型指针的指针。

在构造调用上显式传递给编译器的类型信息不再与add方法要求的输入相匹配。API 不再符合在构造时声明的具体数据。在我看来,这可能会在 API 的工作方式以及底层使用的数据语义方面造成混淆。API 中的这种混淆会导致误用和错误。

结论

阅读这篇文章后,您应该对 Go 中基于结构类型的用户定义类型的泛型语法有更好的理解。您了解了如何声明泛型类型的字段以及如何声明可以指向其他结构类型值的字段。您还看到了如何声明一个可以接受和返回相同泛型类型的值的方法。最后,您将了解在泛型类型的字段和方法声明上强制指针语义如何会造成使用 API 的混乱和误用。

在下一篇文章中,我将探讨当泛型函数需要泛型类型的值来表现行为时,如何声明泛型类型的行为约束。如果您迫不及待,我建议您查看这些博客文章所基于的代码仓库并自己进行实验。如果您有任何问题,请通过电子邮件、Slack 或 Twitter 与我联系。

原文链接:https://www.ardanlabs.com/blog/2020/09/generics-03-struct-types-and-data-semantics.html

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容