Golang 泛型介绍

news2025/1/22 13:14:22

泛型介绍

泛型是一种编写独立于所使用的特定类型的代码的方法。现在可以编写函数和类型(Functions and types)来使用一组类型中的任何一种。

泛型为语言添加了三个重要的东西:

  • 1 函数和类型的类型参数。
  • 2 将接口类型定义为类型集,包括没有方法的类型。
  • 3 类型推断,它允许在调用函数时在许多情况下省略类型参数。

1、类型参数(Type Parameters)

函数和类型现在允许有类型参数类型参数列表与普通参数列表类似,不同之处在于它使用方括号而不是圆括号。(函数名和类型名之后)

为了展示它是如何工作的,让我们从浮点值的基本非泛型Min函数开始:

func Min(x, y float64) float64 {
    if x < y {
        return x
    }
    return y
}

通过添加一个类型参数列表,我们可以使这个函数泛型化——使它适用于不同的类型。在本例中,我们添加了一个带有单个类型参数T的类型参数列表,并将float64的使用替换为T

import "golang.org/x/exp/constraints"

func GMin[T constraints.Ordered](x, y T) T {
    if x < y {
        return x
    }
    return y
}

现在可以用类型参数调用这个函数,方法是编写如下的调用

x := GMin[int](2, 3)

GMin提供类型参数(在本例中为int)称为实例化(instantiation)。实例化分两个步骤进行。首先,编译器在泛型函数或类型中替换它们各自的类型形参(type parameters )的所有类型实参(type arguments )。其次,编译器验证每个类型参数是否满足各自的约束。我们将很快了解这意味着什么,但是如果第二步失败,则实例化失败并且程序无效。

成功实例化之后,我们就有了一个非泛型函数,可以像调用其他函数一样调用它。例如,在类似的代码中

fmin := GMin[float64]
m := fmin(2.71, 3.14)

实例GMin[float64]产生的实际上是我们原始的浮点Min函数,我们可以在函数调用中使用它。

类型参数也可以与类型一起使用。

type Tree[T interface{}] struct {
    left, right *Tree[T]
    value       T
}

func (t *Tree[T]) Lookup(x T) *Tree[T] { ... }

var stringTree Tree[string]

这里,泛型类型Tree存储类型参数 T的值。泛型类型可以有方法,如本例中的Lookup。为了使用泛型类型,它必须被实例化;Tree[string]是一个使用类型实数(type argument )string实例化Tree的例子。

2、类型集合(Type sets)

让我们更深入地了解可用于实例化类型参数的类型实参。

普通函数的每个值形参都有一个类型;该类型定义了一组值。例如,如果我们有一个float64类型,如上面的非泛型函数Min,则允许的参数值集合是可由float64类型表示的浮点值集合。

类似地,类型参数列表对每个类型参数都有一个类型。因为类型形参本身就是一个类型,所以类型形参的类型定义了类型集。这种元类型称为类型约束(type constraint)。

在泛型GMin中,类型约束是从constraints包中导入的。Ordered约束描述了所有类型的集合,这些类型的值可以排序,换句话说,可以与<操作符(或<=>等)进行比较。该约束确保只有具有可排序值的类型才能传递给GMin。这也意味着在GMin函数体中,该类型形参的值可用于与<操作符的比较。

在Go中,类型约束必须是接口(interfaces)。也就是说,接口类型可以用作值类型,也可以用作元类型。接口可以定义方法,因此显然我们可以表达需要提供某些方法的类型约束。但constraints.Ordered也是一个接口类型,<操作符不是一个方法。

为了做到这一点,我们以一种新的方式来看待接口。

直到最近,Go规范还说接口定义了一个方法集,它大致是接口中枚举的方法集。任何实现了所有这些方法的类型都实现了那个接口。

在这里插入图片描述
但是另一种看待这个的方式是说接口定义了一组类型,也就是实现那些方法的类型。从这个角度来看,作为接口类型集元素的任何类型都实现了接口。

在这里插入图片描述
这两个视图导致相同的结果:对于每一组方法,我们可以想象实现这些方法的相应类型集,这就是被接口定义的类型集。

但是,就我们的目的而言,类型集视图比方法集视图有一个优势:我们可以显式地向集合添加类型,从而以新的方式控制类型集。

我们已经扩展了接口类型的语法,以使其工作。例如,interface{ int|string|bool }定义了包含intstringbool类型的类型集。

在这里插入图片描述
换句话说,该接口只满足intstringbool类型。
现在让我们看看 constraints.Ordered的实际定义:

type Ordered interface {
    Integer|Float|~string
}

该声明表明Ordered接口是所有整数、浮点和字符串类型的集合。竖条表示类型的联合(在本例中是类型集)。
IntegerFloat是在constraints包中定义的类似的接口类型。注意,Ordered接口没有定义任何方法。

对于类型约束,我们通常不关心具体的类型,比如string;我们对所有字符串类型都感兴趣。这就是~标记的作用。表达式~string表示底层类型为string的所有类型的集合。这包括string类型本身,以及用type MyString string定义声明的所有类型。

当然,我们仍然希望在接口中指定方法,并且希望向后兼容。在Go 1.18中,接口可以像以前一样包含方法和嵌入接口,但它也可以嵌入非接口类型、联合和底层类型集。

当用作类型约束时,由接口定义的类型集精确地指定允许作为各自类型参数的类型实数的类型。在泛型函数体中,如果操作数的类型是具有约束C的类型实数P,则如果C的类型集中的所有类型都允许操作,则操作数也被允许操作(目前这里有一些实现限制,但普通代码不太可能遇到它们)。

作为约束使用的接口可以指定名称(如Ordered),也可以是内联在类型参数列表中的字面接口。例如:

[S interface{~[]E}, E interface{}]

这里S必须是一个切片类型,它的元素类型可以是任何类型。

由于这是一种常见的情况,对于处于约束位置的接口,可以省略interface{},我们可以简单地这样写:

[S ~[]E, E interface{}]

因为空接口在类型参数列表中很常见,在普通的Go代码中也是如此,所以Go 1.18引入了一个新的预先声明的标识符any作为空接口类型的别名。这样,我们就得到了这个习惯代码:

[S ~[]E, E any]

接口作为类型集是一种强大的新机制,也是在Go语言中实现类型约束的关键。目前,使用新语法形式的接口只能用作约束。但是,不难想象显式类型约束接口在一般情况下是多么有用。

3、类型推理(Type inference)

最后一个新的主要语言特性是类型推断。在某些方面,这是对语言最复杂的改变,但它很重要,因为它让人们在编写调用泛型函数的代码时使用自然的风格。

3.1 函数实参类型推断

有了类型参数,就需要传递类型实参,这可能会导致代码冗长。回到我们的通用GMin函数:

func GMin[T constraints.Ordered](x, y T) T { ... }

类型形参T用于指定普通非类型实参xy的类型。如前所述,可以用显式类型实参调用它:

var a, b, m float64

m = GMin[float64](a, b) // explicit type argument

在许多情况下,编译器可以从普通参数中推断出T的类型参数。这使得代码更短,同时保持清晰。

var a, b, m float64

m = GMin(a, b) // no type argument

这通过将实参ab的类型与形参xy的类型进行匹配来实现。

这种从函数实参的类型推断出类型实参的推理称为函数实参类型推断(function argument type inference)。

函数实参类型推断仅适用于在函数形参中使用的类型形参,而不适用于仅在函数结果或函数体中使用的类型形参。例如,它不适用于像 MakeT[T any]() T这样的函数,因为它们只使用T作为结果。

3.2 约束类型推断

该语言支持另一种类型推断,约束类型推断(constraint type inference)。为了说明这一点,让我们从缩放整数切片的例子开始:

// Scale returns a copy of s with each element multiplied by c.
// This implementation has a problem, as we will see.
func Scale[E constraints.Integer](s []E, c E) []E {
    r := make([]E, len(s))
    for i, v := range s {
        r[i] = v * c
    }
    return r
}

这是一个泛型函数,适用于任何整数类型的切片。

现在假设我们有一个多维Point类型,其中每个Point都是给出该点坐标的整数列表。这种类型自然会有一些方法。

type Point []int32

func (p Point) String() string {
    // Details not important.
}

有时我们想缩放一个Point。由于Point只是整数的切片,我们可以使用前面写的Scale函数:

// ScaleAndPrint doubles a Point and prints it.
func ScaleAndPrint(p Point) {
    r := Scale(p, 2)
    fmt.Println(r.String()) // DOES NOT COMPILE
}

不幸的是,这不能编译,失败的错误,如r.String undefined (type []int32 has no field or method String)

问题就是Scale函数返回类型为[]E的值,其中E是参数slice的元素类型。当使用Point类型的值(其底层类型为[]int32)调用Scale时,返回的值类型为[]int32,而不是Point类型。这是从泛型代码的编写方式得出的,但这不是我们想要的。

为了解决这个问题,我们必须更改Scale函数,以便为切片类型使用类型参数。

// Scale returns a copy of s with each element multiplied by c.
func Scale[S ~[]E, E constraints.Integer](s S, c E) S {
    r := make(S, len(s))
    for i, v := range s {
        r[i] = v * c
    }
    return r
}

我们引入了一个新的类型形参S,它是slice实参的类型。我们对它进行了约束,使得底层类型是S而不是[]E,结果类型现在是S。由于E被约束为整数,其效果与之前相同:第一个参数必须是某种整数类型的切片。函数体的唯一变化是,现在我们在调用make时传递S,而不是[]E

如果使用普通切片调用,则新函数的作用与以前相同,但是如果使用Point类型调用,则返回的是Point类型的值。这就是我们想要的。在这个版本的Scale中,早期的ScaleAndPrint函数将按照我们的预期编译和运行。

但公平地问:为什么可以在不传递显式类型参数的情况下编写对Scale的调用?也就是说,为什么我们可以写Scale(p, 2)而不带类型参数,而不是必须写Scale[Point, int32](p, 2)?我们新的Scale函数有两个类型参数SE。在不传递任何类型参数的Scale调用中,上面描述的函数参数类型推断让编译器推断S的类型参数是Point。但是这个函数还有一个类型参数E它是乘法因子c的类型。对应的函数参数是2,因为2是一个无类型(untyped )常量,函数参数类型推断不能推断出E的正确类型(它最多可能推断出2的默认类型是int,这是不正确的)。相反,编译器推断E的类型参数是切片的元素类型的过程称为约束类型推断(constraint type inference)。

约束类型推断从类型参数约束推导类型参数当一个类型参数具有根据另一个类型参数定义的约束时,使用它。当其中一个类型参数的类型实参已知时,将使用约束来推断另一个类型参数的类型实参。

通常的情况是,当一个约束对某种类型使用~type形式,而该类型是使用其他类型参数编写的。我们在Scale的例子中看到了这一点。S~[]E,它后面是用另一个类型参数表示的类型[]E。如果我们知道S的类型参数,我们就可以推断出E的类型参数。S是切片类型,而E是该切片的元素类型。

这只是对约束类型推断的介绍。有关详细信息,请参阅提案文档或语言规范。

3.3 实践中的类型推理

类型推断如何工作的确切细节是复杂的,但使用它并不复杂:类型推断要么成功,要么失败。如果成功,则可以省略类型参数,并且调用泛型函数看起来与调用普通函数没有什么不同。如果类型推断失败,编译器将给出错误消息,在这种情况下,我们可以只提供必要的类型参数。

在向语言中添加类型推理时,我们试图在推理能力和复杂性之间取得平衡。我们希望确保当编译器推断类型时,这些类型不会令人惊讶。我们尽量小心地避免在未能推断出类型的情况下出错,而不是推断出错误类型。我们可能还没有完全正确,我们可能会在未来的版本中继续完善它。其结果是,可以编写更多不需要显式类型参数的程序。今天不需要类型参数的程序明天也不需要它们。

4、结尾

泛型是1.18中一个重要的新语言特性。这些新的语言变化需要大量的新代码,而这些代码还没有在生产环境中进行过重要的测试。只有当越来越多的人编写和使用泛型代码时,这种情况才会发生。我们相信这个功能实现得很好,质量也很高。然而,与Go的大多数方面不同,我们无法用现实世界的经验来支持这种信念。因此,虽然我们鼓励在有意义的地方使用泛型,但在生产环境中部署泛型代码时,请谨慎使用。

撇开这些谨慎不谈,我们很高兴有了可用的泛型,我们希望它们能让Go程序员更有效率。

22 March 2022

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/434277.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

五项热门技术领域和应用场景

介绍五种当下比较热门的技术&#xff0c;分别是人工智能、云计算、数据分析、微服务和区块链。每种技术都有自己的定义、子领域、应用场景和学习难度。这些技术都有着广阔的发展前景和市场需求&#xff0c;对于想要从事或了解这些领域的人来说&#xff0c;都是很有价值的知识。…

【react 全家桶】初始化脚手架

本人大二学生一枚&#xff0c;热爱前端&#xff0c;欢迎来交流学习哦&#xff0c;一起来学习吧。 <专栏推荐> &#x1f525;&#xff1a;js专栏 &#x1f525;&#xff1a;vue专栏 &#x1f525;&#xff1a;react专栏 文章目录 10 【初始化脚手架】1.什么是 React 脚…

时序数据的内存服务

说明 既要坚定锻炼成熟架构的道路&#xff0c;也要在合理的范围内重塑设计 计算时序数据的特征&#xff0c;少不了“Rolling”类的操作。过去&#xff0c;直接采用pandas进行rolling&#xff0c;效率很不错&#xff0c;但是在实战应用时不太行。 反思下来&#xff1a;离线的操…

Linux --- 软件安装、项目部署

一、软件安装 1.1、软件安装方式 在Linux系统中&#xff0c;安装软件的方式主要有四种&#xff0c;这四种安装方式的特点如下&#xff1a; 1.2、安装JDK 上述我们介绍了Linux系统软件安装的四种形式&#xff0c;接下来我们就通过第一种(二进制发布包)形式来安装 JDK。 JDK…

文案优化技巧,批量文案改写工具

在当今竞争激烈的市场中&#xff0c;一篇优秀的文案可以吸引更多的潜在客户&#xff0c;提高转化率&#xff0c;带来更多的收益。然而&#xff0c;写出优秀的文案有时是一项具有挑战性的任务。许多人不得不花费大量的时间和精力来编辑和重写它们&#xff0c;这不仅耗时费力&…

具有柔性结构的孤岛直流微电网的分级控制(Malab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️❤️&#x1f4a5;&#x1f4a5;&#x1f4a5; &#x1f4cb;&#x1f4cb;&#x1f4cb;本文目录如下&#xff1a;⛳️⛳️⛳️ 目录 1 概述 2 数学…

PICO 4 Pro:加入眼动和面部追踪,VR体验乐趣加倍

VR产品的体验在最近几年得到长足的进展&#xff0c;其中有几个重要的关键点。2019-2020年&#xff0c;VR一体机超越PC VR成为主流&#xff0c;便携性和综合体验做到了极佳的均衡。到了2022年&#xff0c;Pancake光学、彩色VST透视、眼动追踪、面部追踪等技术开始落地&#xff0…

MB510 3BSE002540R1在机器视觉工业领域最基本的应用

​ MB510 3BSE002540R1在机器视觉工业领域最基本的应用 大家都说人类感知外界信息的80%是通过眼睛获得的&#xff0c;图像包含的信息量是最巨大的。那么机器视觉技术的出现&#xff0c;就是为机器设备安上了感知外界的眼睛&#xff0c;使机器具有像人一样的视觉功能&#xff0c…

京东淘宝天猫户外服饰行业数据分析(电商数据查询软件)

户外运动越来越火&#xff0c;甚至还形成了一种独有的穿衣风格——“户外穿搭风”。 冲锋衣、工装裤、工装裙、口袋马甲、渔夫帽等都是这两年在这种户外穿搭风潮席卷之下爆红的产物。无论是在京东还是淘宝天猫&#xff0c;这类服饰的销售表现都比较出色。 京东数据&#xff1a;…

Spark大数据处理学习笔记(3.1)掌握RDD的创建

该文章主要为完成实训任务&#xff0c;详细实现过程及结果见【http://t.csdn.cn/oT0of】 文章目录 一、准备工作1.1 准备文件1.1.1 准备本地系统文件1.1.2 启动HDFS服务1.1.3 上传文件到HDFS 1.2 启动Spark Shell1.2.1 启动Spark服务1.2.2 启动Spark Shell 二、创建RDD2.1 通过…

Java语言背景介绍 及 语言跨平台原理

01_Java语言背景介绍 Java语言的三个版本&#xff1a; ●Java SE ● Java ME ●Java EE Java SE&#xff1a; Java语言的&#xff08;标准版&#xff09;&#xff0c;用于桌面应用的开发&#xff0c;是其他两个版本的基础。 桌面应用&#xff1a;GUI程序&#xff0c;是采…

科普丨关于 A/B 测试的十问十答

你想知道的&#xff0c;都在这里&#xff01;本文是神策数据「十问十答」科普系列文章的第二期&#xff0c;围绕 A/B 测试展开。 1 Q&#xff1a;什么是 A/B 测试&#xff1f; A&#xff1a;A/B 测试作为互联网企业的核心增长手段之一&#xff0c;其价值已在实际应用中被多次验…

ERTEC200P-2 PROFINET设备完全开发手册(6-2)

6.2 诊断与报警实验 首先确认固件为 App1_STANDARD, 将宏定义改为&#xff1a; #define EXAMPL_DEV_CONFIG_VERSION 1 参照第6节的内容&#xff0c;编译和调试固件&#xff0c;并在TIA Portal 中建立RT项目。启动固件后&#xff0c;TIA Portal 切换到在线&#xff0c;可以看…

springboot+vue人职匹配推荐系统(源码+文档)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的人职匹配推荐系统。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 目前有各类成品java毕设&#xff0c;需要请看文…

《离散数学导学》精炼——第9章(函数)

学习是一个长久而艰苦的过程&#xff0c;但不学习则更艰苦。 文章目录 引言正文函数的定义全函数覆盖运算函数的性质&#xff08;重点&#xff09;单射满射双射 递归函数良定义 引言 笔者一直觉得在计算机这一学科的学习中&#xff0c;离散数学是极为重要的知识基础。离散化的…

node中模块化

目录 模块化概念模块化规范Node.js 中模块的分类加载模块 模块作用域module对象module.exports 对象exports 对象 Node.js中的模块化规范 模块化概念 模块化是指解决一个复杂问题时&#xff0c;自顶向下逐层把系统划分成若干模块的过程。对于整个系统来说&#xff0c;模块是可…

SpringBoot的Filter过滤器结合JWT实现登录验证

概念&#xff1a;Filter 过滤器&#xff0c;是 JavaWeb 三大组件(Servlet、Filter、Listener)之一。 过滤器可以把对资源的请求拦截下来&#xff0c;从而实现一些特殊的功能。 过滤器一般完成一些通用的操作&#xff0c;比如&#xff1a;登录校验、统一编码处理、敏感字符处理等…

看完这篇文章你就彻底懂啦{保姆级讲解}-----(LeetCode刷题27移除元素) 2023.4.18

目录 前言算法题&#xff08;LeetCode 27.移除元素&#xff09;—&#xff08;保姆级别讲解&#xff09;分析题目算法思想&#xff08;重要&#xff09;暴力解法代码&#xff1a;双指针法&#xff08;快慢指针法&#xff09;代码&#xff1a;反思 结束语 前言 本文章一部分内容…

UDP - C/S模型

由于UDP不需要维护连接&#xff0c;程序逻辑简单了很多&#xff0c;但是UDP协议是不可靠的&#xff0c;保证通讯可靠性的机制需要在应用层实现。 通信函数 ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen); 参…

无人机动力系统优化测试平台-15kg级-Flight Stand 15

产品简介 通过Flight Stand 15测试台对电机和螺旋桨的拉力&#xff0c;扭矩&#xff0c;转速&#xff0c;电流&#xff0c;电压&#xff0c;温度&#xff0c;空速&#xff0c;螺旋桨效率和电机效率的测量帮助您精准地描述和评估其性能参数&#xff0c;这是我们五年多来的无人机…