泛型第 02 部分:基础类型

变化

该草案是一份变化文件,这意味着这些提案需要随着时间的推移而改变。本节记录此提案何时发生更改。

2020/08/21 :推进泛型设计草案

介绍

在上一篇文章中,我向您展示了如何使用泛型草案文档中建议的语法在 Go 中编写泛型函数。我通过使用具体类型、空接口和最后的泛型编写同一函数的不同版本的过程来做到这一点。我还提供了有关为什么需要新语法以及编译器如何通过在调用站点推断类型信息来简化泛型函数调用的信息。

在这篇文章中,我将分享一个基于泛型底层类型声明类型的示例,并进一步分解新语法。我将使用与上一篇文章中类似的进展,以不同的方式声明相同的类型。你可以在这个playground链接找到这篇文章的代码。

具体示例

如果您需要定义一个使用整数的向量怎么办?

清单 1.

16 type vectorInt []int
17
18 func (v vectorInt) last() (int, error) {
19     if len(v) == 0 {
20         return 0, errors.New("empty")
21     }
22     return v[len(v)-1], nil
23 }

69 func main() {
70     fmt.Print("vectorInt : ")
71     vInt := vectorInt{10, -1}
72     i, err := vInt.last()
73     if i < 0 {
74         fmt.Print("negative integer: ")
75     }
76     fmt.Printf("value: %d error: %v\n", i, err)

Output:
vectorInt : negative integer: value: -1 error: <nil>

在清单 1 中,在第 16 行定义了一个名为vectorInt的类型,其基础类型是int。在第 18 行,使用值接收器定义了一个名为last的方法,该接收器返回存储在向量中最高索引位置的整数。如果向量为空,则返回整数的零值并返回错误。

在 main 函数的第 71 行,您可以看到如何构造一个vectorInt用两个整数值初始化的类型的值。然后在第 72 行执行调用last并返回整数值 -1。在第 73 行,检查返回值是否为负数,然后在第 76 行显示该值。

如果您的应用程序现在需要使用字符串向量怎么办?

清单 2.

25 type vectorString []string
26
27 func (v vectorString) last() (string, error) {
28     if len(v) == 0 {
29         return "", errors.New("empty")
30     }
31     return v[len(v)-1], nil
32 }

69 func main() {
...
78     fmt.Print("vectorString : ")
79     vStr := vectorString{"A", "B", string([]byte{0xff})}
80     s, err := vStr.last()
81     if !utf8.ValidString(s) {
82         fmt.Print("non-valid string: ")
83     }
84     fmt.Printf("value: %q error: %v\n", s, err)

Output:
vectorString : non-valid string: value: "\xff" error: <nil>

在清单 2 中,在第 25 行定义了一个名为vectorString的类型,其基础类型是string. 在第 27 行,该last方法定义了一个值接收器,该接收器返回存储在向量中最高索引位置的字符串。如果向量为空,则返回字符串的零值和错误。

在 main 函数的第 79 行,您可以看到如何构造一个vectorString用三个字符串值初始化的类型的值。然后在第 80 行执行调用last并返回无效字符串。在第 81 行,检查返回值以验证字符串是否完全由有效的 UTF-8 编码符文组成,然后在第 84 行上,该值显示为带引号。

如果您的应用程序现在需要使用浮点向量怎么办?在这一点上,你需要复制的整数执行和更换intfloat64,改变了变量名。

这两个实现之间唯一真正的区别是每个都只适用于不同的具体类型,并且这种区别需要新的实现。积极的事情是您可以对返回的值执行基于特定类型的验证,因为您直接使用已知且特定的具体类型的值。

空接口

有没有办法定义一个可以处理整数、字符串和浮点数的向量类型?

清单 3.

41 type vectorInterface []interface{}
42
43 func (v vectorInterface) last() (interface{}, error) {
44     if len(v) == 0 {
45         return nil, errors.New("empty")
46     }
47     return v[len(v)-1], nil
48 }

清单 3 显示了一个向量类型的实现,它可以处理整数、字符串、浮点数以及任何类型的数据。再一次,这个和其他两种向量类型之间的唯一区别是底层切片使用空接口。

清单 4.

 69 func main() {
...
 88     fmt.Print("vectorInterface : ")
 89     vItf := vectorInterface{10, "A", 20, "B", 3.14}
 90     itf, err := vItf.last()
 91     switch v := itf.(type) {
 92     case int:
 93         if v < 0 {
 94             fmt.Print("negative integer: ")
 95         }
 96     case string:
 97         if !utf8.ValidString(v) {
 98             fmt.Print("non-valid string: ")
 99         }
100     default:
101         fmt.Printf("unknown type %T: ", v)
102     }
103     fmt.Printf("value: %v error: %v\n", itf, err)

Output:
vectorInterface : unknow type float64: value: 3.14 error: <nil>

在清单 4 的第 89 行,您可以看到空接口版本如何允许在构造期间存储多种类型的数据。在这个例子中,我混合了整数、字符串和浮点数的存储。这使得向量更灵活,但也更复杂。

查看第 91 到 102 行。如果我想检测返回的最后一个值是否为负整数,我需要执行类型断言。检测返回的最后一个值是否为有效字符串也是如此。我不再保证向量正在处理特定类型的数据。这使得使用向量更加复杂和容易出错。

泛 型

使用泛型,您可以定义一个向量类型,该类型适用于任何类型的值,其中向量的每个构造都仅限于单一类型的数据。

清单 5.

57 type vector[T any] []T
58
59 func (v vector[T]) last() (T, error) {
60     var zero T
61     if len(v) == 0 {
62         return zero, errors.New("empty")
63     }
64     return v[len(v)-1], nil
65 }

清单 5 显示了一个通用向量类型,它将向量的构造限制为单一类型的数据。如上一篇文章所述,方括号用于声明该类型是在编译时确定的泛型类型T。约束使用any描述了对类型没有约束。

在第 59 行,last使用类型的值接收器声明该方法,vector[T]以表示具有vector某种类型的底层切片的类型值T。该方法返回相同类型的值T

清单 6.

 68 func main() {
...
107    fmt.Print("vector[int] : ")
108    vGenInt := vector[int]{10, -1}
109    i, err = vGenInt.last()
110    if i < 0 {
111        fmt.Print("negative integer: ")
112    }
113    fmt.Printf("value: %d error: %v\n", i, err)
114
115    fmt.Print("vector[string] : ")
116    vGenStr := vector[string]{"A", "B", string([]byte{0xff})}
117    s, err = vGenStr.last()
118    if !utf8.ValidString(s) {
119        fmt.Print("non-valid string: ")
120    }
121    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>

清单 6 展示了如何使用第 108 行的int和第 116行的string基础类型构造vector类型的值。此代码的一个重要方面是构造调用。

清单 7.

// Zero Value Construction
var vGenInt vector[int]
var vGenStr vector[string]

// Non-Zero Value Construction
vGenInt := vector[int]{10, -1}
vGenStr := vector[string]{"A", "B", string([]byte{0xff})}

清单 7 显示了在构造过程中如何显式提供要使用的类型T。在将这些泛型类型构造为零值时,编译器不可能推断出类型。然而,在构造过程中初始化的情况下,编译器可以推断类型,但工具目前没有实现这一点。

现在,如果您的应用程序需要一个浮点向量,您可以在构造时声明它,而无需编写任何新代码。

清单 8.

func main() {
    fmt.Print("vector[float64] : ")
    vGenFlt := vector[float64]{10.45, -1.32}
    f, err := vGenFlt.last()
    if f < 0.0 {
        fmt.Print("negative float: ")
    }
    fmt.Printf("value: %f error: %v\n", f, err)

清单 8 展示了如何使用浮点向量。所需要的只是float64在构造时提供类型以表示T实现中的类型。检查负值时不需要类型断言,因为该向量中的所有数据都被限制为相同类型的float64

零值

规范的一个方面侧重于将泛型类型构造为零值状态。

清单 9.

56 type vector[T any] []T
57
58 func (v vector[T]) last() (T, error) {
59     var zero T
60     if len(v) == 0 {
61         return zero, errors.New("empty")
62     }
63     return v[len(v)-1], nil
64 }

在清单 9 中的第 58 行,关注方法声明last以及该方法如何返回泛型类型的值T。现在看第 61 行,函数需要返回一个零值。在这种情况下,您需要为类型返回零值T

当前的草案提供了两种解决方案来编写此代码。您在清单 9 中看到的第一个解决方案。在第 59 行,一个名为zero的变量被构造为其类型的零值状态,然后该变量用于第 61 行的返回。

另一种选择是使用内置函数new并在语句中引用返回的指针return

清单 10.

58 func (v vector[T]) last() (T, error) {
59
60     if len(v) == 0 {
61         return *new(T), errors.New("empty")
62     }
63     return v[len(v)-1], nil
64 }

清单 10 显示了return对第 61 行语句的更改。这个版本last使用内置函数new来构造零值并取消引用返回的指针以满足返回类型T

注意:你可能会想为什么不使用T{}来执行零值构造呢?问题是这种语法不适用于所有类型,例如标量类型(int、string、bool)。所以这不是一个选择。

在某些时候,社区需要决定哪个选项应该被视为最佳实践。在此第一个版本之前,可能会提供其他选项。

结论

阅读本文后,您应该对 Go 中基于泛型基础类型的用户定义类型的基本语法有了更好的了解。在声明泛型类型时,您应该看到需要方括号来形成泛型类型列表。在构造泛型类型时,您了解了必须如何显式传递类型信息,尽管草稿确实支持为非零值构造调用推断类型的功能。

在下一篇文章中,我将探讨如何使用泛型来定义具有结构的用户定义类型。我还将通过此示例探讨值和指针语义。如果您等不及了,我建议您查看这些博客文章所基于的代码存储库,并亲自进行实验。如果您有任何疑问,请通过电子邮件,Slack或Twitter与我联系。

原文链接:https://www.ardanlabs.com/blog/2020/08/generics-02-underlying-types.html

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容