Go1.18新特性之泛型实例代码分析


本篇内容主要讲解“Go1.18新特性之泛型实例代码分析”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Go1.18新特性之泛型实例代码分析”吧!假设我们有个计算两数之和的函数这个函数很简单,但是它有个问题——无法计算int类型之外的和。如果我们想计算浮点或者字符串的和该怎么办?解决办法之一就是像下面这样为不同类型定义不同的函数可是除此之外还有没有更好的方法?答案是有的,我们可以来回顾下函数的形参(parameter)实参(argument)这一基本概念:我们知道,函数的形参(parameter)只是类似占位符的东西并没有具体的值,只有我们调用函数传入实参(argument)之后才有具体的值。那么,如果我们将形参 实参这个概念推广一下,给变量的类型也引入和类似形参实参的概念的话,问题就迎刃而解:在这里我们将其称之为类型形参(type parameter)类型实参(type argument),如下:在上面这段伪代码中, T 被称为类型形参(type parameter), 它不是具体的类型,在定义函数时类型并不确定。因为 T 的类型并不确定,所以我们需要像函数的形参那样,在调用函数的时候再传入具体的类型。这样我们不就能一个函数同时支持多个不同的类型了吗?在这里被传入的具体类型被称为类型实参(type argument):下面一段伪代码展示了调用函数时传入类型实参的方式:通过引入类型形参类型实参这两个概念,我们让一个函数获得了处理多种不同类型数据的能力,这种编程方式被称为 泛型编程。可能你会已奇怪,我通过Go的 接口+反射 不也能实现这样的动态数据处理吗?是的,泛型能实现的功能通过接口+反射也基本能实现。但是使用过反射的人都知道反射机制有很多问题:1、 用起来麻烦2、失去了编译时的类型检查,不仔细写容易出错3、性能不太理想而在泛型适用的时候,它能解决上面这些问题。但这也不意味着泛型是万金油,泛型有着自己的适用场景,当你疑惑是不是该用泛型的话,请记住下面这条经验:如果你经常要分别为不同的类型写完全相同逻辑的代码,那么使用泛型将是最合适的选择通过上面的伪代码,我们实际上已经对Go的泛型编程有了最初步也是最重要的认识—— 类型形参 和 类型实参。而Go1.18也是通过这种方式实现的泛型,但是单纯的形参实参是远远不能实现泛型编程的,所以Go还引入了非常多全新的概念:1、类型形参 (Type parameter)2、类型实参(Type argument)3、类型形参列表( Type parameter list)4、类型约束(Type constraint)5、实例化(Instantiations)6、泛型类型(Generic type)7、泛型接收器(Generic receiver)8、泛型函数(Generic function)等等等等。啊,实在概念太多了头晕?没事请跟着我慢慢来,首先从泛型类型(generic type)讲起。观察下面这个简单的例子:这里定义了一个新的类型IntSlice,它的底层类型是[]int,理所当然只有int类型的切片能赋值给 IntSlice 类型的变量。接下来如果我们想要定义一个可以容纳float32string等其他类型的切片的话该怎么办?很简单,给每种类型都定义个新类型:但是这样做的问题显而易见,它们结构都是一样的只是成员类型不同就需要重新定义这么多新类型。那么有没有一个办法能只定义一个类型就能代表上面这所有的类型呢?答案是可以的,这时候就需要用到泛型了:不同于一般的类型定义,这里类型名称 Slice 后带了中括号,对各个部分做一个解说就是:1、 T 就是上面介绍过的类型形参(Type parameter),在定义Slice类型的时候 T 代表的具体类型并不确定,类似一个占位符2、int|float32|float64这部分被称为类型约束(Type constraint),中间的 | 的意思是告诉编译器,类型形参 T 只可以接收 int 或 float32 或 float64 这三种类型的实参3、 中括号里的T int|float32|float64这一整串因为定义了所有的类型形参(在这个例子里只有一个类型形参T),所以我们称其为类型形参列表(type parameter list)4、 这里新定义的类型名称叫 Slice[T]这种类型定义的方式中带了类型形参,很明显和普通的类型定义非常不一样,所以我们将这种类型定义中带类型形参的类型,称之为泛型类型(Generic type)泛型类型不能直接拿来使用,必须传入类型实参(Type argument)将其确定为具体的类型之后才可使用。而传入类型实参确定具体类型的操作被称为实例化(Instantiations):对于上面的例子,我们先给泛型类型Slice[T]传入了类型实参int,这样泛型类型就被实例化为了具体类型Slice[int],被实例化之后的类型定义可近似视为如下:我们用实例化后的类型Slice[int]定义了一个新的变量 a ,这个变量可以存储int类型的切片。之后我们还用同样的方法实例化出了另一个类型 Slice[float32] ,并创建了变量 b 。因为变量 a 和 b 就是具体的不同类型了(一个 Slice[int] ,一个 Slice[float32]),所以 a = b 这样不同类型之间的变量赋值是不允许的。同时,因为 Slice[T] 的类型约束限定了只能使用 int 或 float32 或 float64 来实例化自己,所以Slice[string]这样使用 string 类型来实例化是错误的。上面只是个最简单的例子,实际上类型形参的数量可以远远不止一个,如下:用上面的例子重新复习下各种概念的话:1、KEY和VALUE是类型形参2、int|string 是KEY的类型约束, float32|float64 是VALUE的类型约束3、KEY int|string, VALUE float32|float64 整个一串文本因为定义了所有形参所以被称为类型形参列表4、Map[KEY, VALUE] 是泛型类型,类型的名字就叫 Map[KEY, VALUE]5、var a MyMap[string, float64] = xx 中的string和float64是类型实参,用于分别替换KEY和VALUE,实例化出了具体的类型 MyMap[string, float64]还有点头晕?没事,的确一下子有太多概念了,这里用一张图就能简单说清楚:所有类型定义都可使用类型形参,所以下面这种结构体以及接口的定义也可以使用类型形参:类型形参是可以互相套用的,如下这个例子看起来有点复杂且难以理解,但实际上只要记住一点:任何泛型类型都必须传入类型实参实例化才可以使用。所以我们这就尝试传入类型实参看看:上面的代码中,我们为T传入了实参int,然后因为 S 的定义是[]T,所以 S 的实参自然是[]int。经过实例化之后 WowStruct[T,S] 的定义类似如下:因为 S 的定义是 []T ,所以 T 一定决定了的话 S 的实参就不能随便乱传了,下面这样的代码是错误的:定义泛型类型的时候,基础类型不能只有类型形参,如下:当类型约束的一些写法会被编译器误认为是表达式时会报错。如下:为了避免这种误解,解决办法就是给类型约束包上 interface{} 或加上逗号消除歧义(关于接口具体的用法会在后半篇提及)因为上面逗号的用法限制比较大,这里推荐统一用 interface{} 解决问题。这里讨论种比较特殊的泛型类型,如下:这里虽然使用了类型形参,但因为类型定义是type Wow[T int|string] int,所以无论传入什么类型实参,实例化后的新类型的底层类型都是 int 。所以int类型的数字123可以赋值给变量a和b,但string类型的字符串 “hello” 不能赋值给c这个例子没有什么具体意义,但是可以让我们理解泛型类型的实例化的机制。泛型和普通的类型一样,可以互相嵌套定义出更加复杂的新类型,如下:观察下面两种类型约束的写法仅限于这个例子,这两种写法和实现的功能其实是差不多的,实例化之后结构体相同。但是像下面这种情况的时候,我们使用前一种写法会更好:我们有时候会经常用到匿名的结构体,并在定义好匿名结构体之后直接初始化:所以在使用泛型的时候我们只能放弃使用匿名结构体,对于很多场景来说这会造成麻烦(最主要麻烦集中在单元测试的时候,为泛型做单元测试会非常麻烦,这点我之后的文章将会详细阐述)。看了上的例子,你一定会说,介绍了这么多复杂的概念,但好像泛型类型根本没什么用处啊?是的,单纯的泛型类型实际上对开发来说用处并不大。但是如果将泛型类型和接下来要介绍的泛型receiver相结合的话,泛型就有了非常大的实用性了我们知道,定义了新的普通类型之后可以给类型添加方法。那么可以给泛型类型添加方法吗?答案自然是可以的,如下:这个例子为泛型类型MySlice[T]添加了一个计算成员总和的方法Sum()。注意观察这个方法的定义:首先看receiver (s MySlice[T]),所以我们直接把类型名称MySlice[T]写入了receiver中 然后方法的返回参数我们使用了类型形参 T **(实际上如果有需要的话,方法的接收参数也可以实用类型形参) 在方法的定义中,我们也可以使用类型形参 T (在这个例子里,我们通过 var sum T 定义了一个新的变量 sum ) 对于这个泛型类型 MySlice[T] 我们该如何使用?还记不记得之前强调过很多次的,泛型类型无论如何都需要先用类型实参实例化,所以用法如下:该如何理解上面的实例化?首先我们用类型实参 int 实例化了泛型类型MySlice[T],所以泛型类型定义中的所有 T 都被替换为 int,最终我们可以把代码看作下面这样:用 float32 实例化和用 int 实例化同理,此处不再赘述。通过泛型receiver,泛型的实用性一下子得到了巨大的扩展。在没有泛型之前如果想实现通用的数据结构,诸如:堆、栈、队列、链表之类的话,我们的选择只有两个:为每种类型写一个实现1、使用 接口+反射2、而有了泛型之后,我们就能非常简单地创建通用数据结构了。接下来用一个更加实用的例子 —— 队列 来讲解。队列是一种先入先出的数据结构,它和现实中排队一样,数据只能从队尾放入、从队首取出,先放入的数据优先被取出来为了方便说明,上面是队列非常简单的一种实现方法,没有考虑线程安全等很多问题 Queue[T] 因为是泛型类型,所以要使用的话必须实例化,实例化与使用方法如下所示:使用接口的时候经常会用到类型断言或 type swith 来确定接口具体的类型,然后对不同类型做出不同的处理,如:那么你一定会想到,对于 valut T 这样通过类型形参定义的变量,我们能不能判断具体类型然后对不同类型做出不同处理呢?答案是不允许的,如下:虽然type switch和类型断言不能用,但我们可通过反射机制达到目的:这看起来达到了我们的目的,可是当你写出上面这样的代码时候就出现了一个问题:你为了避免使用反射而选择了泛型,结果到头来又为了一些功能在在泛型中使用反射 当出现这种情况的时候你可能需要重新思考一下,自己的需求是不是真的需要用泛型(毕竟泛型机制本身就很复杂了,再加上反射的复杂度,增加的复杂度并不一定值得)当然,这一切选择权都在你自己的手里,根据具体情况斟酌。在介绍完泛型类型和泛型receiver之后,我们来介绍最后一个可以使用泛型的地方——泛型函数。有了上面的知识,写泛型函数也十分简单。假设我们想要写一个计算两个数之和的函数:这个函数理所当然只能计算int的和,而浮点的计算是不支持的。这时候我们可以像下面这样定义一个泛型函数:上面就是泛型函数的定义。这种带类型形参的函数被称为泛型函数 它和普通函数的点不同在于函数名之后带了类型形参。这里的类型形参的意义、写法和用法因为与泛型类型是一模一样的,就不再赘述了。和泛型类型一样,泛型函数也是不能直接调用的,要使用泛型函数的话必须传入类型实参之后才能调用。自动推导的写法就好像免去了传入实参的步骤一样,但请记住这仅仅只是编译器帮我们推导出了类型实参,实际上传入实参步骤还是发生了的。在Go中我们经常会使用匿名函数,如:那么Go支不支持匿名泛型函数呢?答案是不能——匿名函数不能自己定义类型形参:但是匿名函数可以使用别处定义好的类型实参,如:既然函数都支持泛型了,那你应该自然会想到,方法支不支持泛型?很不幸,目前Go的方法并不支持泛型,如下:讲完了泛型类型、泛型receiver、泛型函数后,Go的泛型算是介绍完一半多了。在这里我们做一个概念的小结:1、Go的泛型(或者或类型形参)目前可使用在3个地方泛型类型 – 类型定义中带类型形参的类型泛型receiver – 泛型类型的receiver泛型函数 – 带类型形参的函数2、为了实现泛型,Go引入了一些新的概念:类型形参类型形参列表类型实参类型约束实例化 – 泛型类型不能直接使用,要使用的话必须传入类型实参进行实例化什么,这文章已经很长很复杂了,才讲了一半?是的,Go这次1.18引入泛型为语言增加了较大的复杂度,目前还只是新概念的介绍,下面后半段将介绍Go引入泛型后对接口 做出的重大调整。那么做好心理准备,我们出发吧。有时候使用泛型编程时,我们会书写长长的类型约束,如下:理所当然,这种写法是我们无法忍受也难以维护的,而Go支持将类型约束单独拿出来定义到接口中,从而让代码更容易维护:这段代码把类型约束给单独拿出来,写入了接口类型IntUintFloat当中。需要指定类型约束的时候直接使用接口IntUintFloat即可。不过这样的代码依旧不好维护,而接口和接口、接口和普通类型之间也是可以通过 | 进行组合:上面的代码中,我们分别定义了 Int, Uint, Float 三个接口类型,并最终在 Slice[T] 的类型约束中通过使用 | 将它们组合到一起。同时,在接口里也能直接组合其他接口,所以还可以像下面这样:上面定义的 Slie[T] 虽然可以达到目的,但是有一个缺点:这里发生错误的原因是,泛型类型 Slice[T] 允许的是 int 作为类型实参,而不是 MyInt (虽然 MyInt 类型底层类型是 int ,但它依旧不是 int 类型)。为了从根本上解决这个问题,Go新增了一个符号 ~ ,在类型约束中使用类似 ~int 这种写法的话,就代表着不光是 int ,所有以 int 为底层类型的类型也都可用于实例化。使用 ~ 对代码进行改写之后如下:限制:使用~时有一定的限制:1、~后面的类型不能为接口2、~后面的类型必须为基本类型上面的例子中,我们学习到了一种接口的全新写法,而这种写法在Go1.18之前是不存在的。如果你比较敏锐的话,一定会隐约认识到这种写法的改变这也一定意味着Go语言中接口(interface)这个概念发生了非常大的变化。是的,在Go1.18之前,Go官方对接口(interface)的定义是:接口是一个方法集(method set)An interface type specifies a method set called its interface就如下面这个代码一样,ReadWriter接口定义了一个接口(方法集),这个集合中包含了Read()Write()这两个方法。所有同时定义了这两种方法的类型被视为实现了这一接口。但是,我们如果换一个角度来重新思考上面这个接口的话,会发现接口的定义实际上还能这样理解:我们可以把 ReaderWriter 接口看成代表了一个 类型的集合,所有实现了Read() Writer()这两个方法的类型都在接口代表的类型集合当中。通过换个角度看待接口,在我们眼中接口的定义就从方法集(method set)变为了类型集(type set)。而Go1.18开始就是依据这一点将接口的定义正式更改为了 **类型集(Type set)**。你或许会觉得,这不就是改了下概念上的定义实际上没什么用吗?是的,如果接口功能没变化的话确实如此。但是还记得下面这种用接口来简化类型约束的写法吗:这就体现出了为什么要更改接口的定义了。用 类型集 的概念重新理解上面的代码的话就是:接口类型 Float 代表了一个 类型集合, 所有以 float32 或 float64 为底层类型的类型,都在这一类型集之中而type Slice[T Float] []T中, 类型约束 的真正意思是:类型约束 指定了类型形参可接受的类型集合,只有属于这个集合中的类型才能替换形参用于实例化 如:6.2.1 接口实现(implement)定义的变化既然接口定义发生了变化,那么从Go1.18开始接口实现(implement)的定义自然也发生了变化:当满足以下条件时,我们可以说类型 T 实现了接口 I ( type T implements interface I):1、T 不是接口时:类型 T 是接口 I 代表的类型集中的一个成员 (T is an element of the type set of I)。2、T 是接口时: T 接口代表的类型集是 I 代表的类型集的子集(Type set of T is a subset of the type set of I)。6.2.2 类型的并集并集我们已经很熟悉了,之前一直使用的|符号就是求类型的并集(union)6.2.3 类型的交集接口可以不止书写一行,如果一个接口有多行类型定义,那么取它们之间的交集。上面这个例子中1、接口 A 代表的是 AllInt 与 Uint 的 交集,即~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64。2、接口 B 代表的则是 AllInt 和 ~int 的交集,即~int。除了上面的交集,下面也是一种交集:很显然,~int 和 int 的交集只有int一种类型,所以接口C代表的类型集中只有int一种类型。6.2.4 空集当多个类型的交集如下面Bad这样为空的时候,Bad这个接口代表的类型集为一个空集:没有任何一种类型属于空集。虽然 Bad 这样的写法是可以编译的,但实际上并没有什么意义。6.2.5 空接口和 any上面说了空集,接下来说一个特殊的类型集——空接口interface{}。因为,Go1.18开始接口的定义发生了改变,所以interface{}的定义也发生了一些变更:空接口代表了所有类型的集合所以,对于Go1.18之后的空接口应该这样理解:1、虽然空接口内没有写入任何的类型,但它代表的是所有类型的集合,而非一个空集。2、类型约束中指定空接口的意思是指定了一个包含所有类型的类型集,并不是类型约束限定了只能使用空接口来做类型形参。因为空接口是一个包含了所有类型的类型集,所以我们经常会用到它。于是,Go1.18开始提供了一个和空接口interface{}等价的新关键词any,用来使代码更简单:实际上any的定义就位于Go语言的builtin.go文件中(参考如下),any实际上就是interaface{}的别名(alias),两者完全等价。所以从 Go 1.18 开始,所有可以用到空接口的地方其实都可以直接替换为any,如:如果你高兴的话,项目迁移到 Go1.18 之后可以使用下面这行命令直接把整个项目中的空接口全都替换成 any。当然因为并不强制,所以到底是用interface{}还是any全看自己喜好。Go语言项目中就曾经有人提出过把Go语言中所有 interface{ }替换成 any 的 issue,然后因为影响范围过大过而且影响因素不确定,理所当然被驳回了。6.2.6 comparable(可比较) 和 可排序(ordered)对于一些数据类型,我们需要在类型约束中限制只接受能!===对比的类型,如map:所以Go直接内置了一个叫comparable的接口,它代表了所有可用!=以及==对比的类型:compar免费云主机域名able比较容易引起误解的一点是很多人容易把他与可排序搞混淆。可比较指的是 可以执行!= ==操作的类型,并没确保这个类型可以执行大小比较(>,=)。如下:而可进行大小比较的类型被称为Orderd。目前Go语言并没有像comparable这样直接内置对应的关键词,所以想要的话需要自己来定义相关接口,比如我们可以参考Go官方包golang.org/x/exp/constraints如何定义:这里虽然可以直接使用官方包 golang.org/x/exp/constraints ,但因为这个包属于实验性质的 x 包,今后可能会发生非常大变动,所以并不推荐直接使用我们接下来再观察一个例子,这个例子是阐述接口是类型集最好的例子:最开始看到这一例子你一定有点懵不太理解它代表的意思,但是没关系,我们用类型集的概念就能比较轻松理解这个接口的意思:接口类型 ReadWriter 代表了一个类型集合,所有以 string 或 []rune 为底层类型,并且实现了 Read() Write() 这两个方法的类型都在 ReadWriter 代表的类型集当中。如下面代码中,StringReadWriter 存在于接口 ReadWriter 代表的类型集中,而 BytesReadWriter 因为底层类型是 []byte(既不是string也是不[]rune) ,所以它不属于 ReadWriter 代表的类型集你一定会说,啊等等,这接口也变得太复杂了把,那我定义一个 ReadWriter 类型的接口变量,然后接口变量赋值的时候不光要考虑到方法的实现,还必须考虑到具体底层类型?心智负担也太大了吧。是的,为了解决这个问题也为了保持Go语言的兼容性,Go1.18开始将接口分为了两种类型。1、 基本接口(Basic interface)2、一般接口(General interface)6.3.1 基本接口(Basic interface)接口定义中如果只有方法的话,那么这种接口被称为基本接口(Basic interface)。这种接口就是Go1.18之前的接口,用法也基本和Go1.18之前保持一致。基本接口大致可以用于如下几个地方:最常用的,定义接口变量并赋值基本接口因为也代表了一个类型集,所以也可用在类型约束中6.3.2 一般接口(General interface)如果接口内不光只有方法,还有类型的话,这种接口被称为 一般接口(General interface) ,如下例子都是一般接口:一般接口类型不能用来定义变量,只能用于泛型的类型约束中。所以以下的用法是错误的:这一限制保证了一般接口的使用被限定在了泛型之中,不会影响到Go1.18之前的代码,同时也极大减少了书写代码时的心智负担所有类型的定义中都可以使用类型形参,所以接口定义自然也可以使用类型形参,观察下面这两个例子:因为引入了类型形参,所以这两个接口是泛型类型。而泛型类型要使用的话必须传入类型实参实例化才有意义。所以我们来尝试实例化一下这两个接口。因为 T 的类型约束是 any,所以可以随便挑一个类型来当实参(比如string):经过实例化之后就好理解了, DataProcessor[string] 因为只有方法,所以它实际上就是个 基本接口(Basic interface),这个接口包含两个能处理string类型的方法。像下面这样实现了这两个能处理string类型的方法就算实现了这个接口:再用同样的方法实例化DataProcessor2[T]DataProcessor2[string]因为带有类型并集所以它是 一般接口(General interface),所以实例化之后的这个接口代表的意思是:1、只有实现了Process(string) stringSave(string) error这两个方法,并且以intstruct{ Data interface{} }为底层类型的类型才算实现了这个接口。2、一般接口(General interface)不能用于变量定义只能用于类型约束,所以接口 DataProcessor2[string] 只是定义了一个用于类型约束的类型集。Go1.18从开始,在定义类型集(接口)的时候增加了非常多十分琐碎的限制规则,其中很多规则都在之前的内容中介绍过了,但剩下还有一些规则因为找不到好的地方介绍,所以在这里统一介绍下:用|连接多个类型的时候,类型之间不能有相交的部分(即必须是不交集):但是相交的类型中是接口的话,则不受这一限制:类型的并集中不能有类型形参接口不能直接或间接地并入自己接口的并集成员个数大于一的时候不能直接或间接并入 comparable 接口带方法的接口(无论是基本接口还是一般接口),都不能写入接口的并集中:到此,相信大家对“Go1.18新特性之泛型实例代码分析”有了更深的了解,不妨来实际操作一番吧!这里是百云主机网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

相关推荐: tensorflow转onnx如何实现

本文小编为大家详细介绍“tensorflow转onnx如何实现”,内容详细,步骤清晰,细节处理妥当,希望这篇“tensorflow转onnx如何实现”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。安装tf2onnx以及onnxrunt…

免责声明:本站发布的图片视频文字,以转载和分享为主,文章观点不代表本站立场,本站不承担相关法律责任;如果涉及侵权请联系邮箱:360163164@qq.com举报,并提供相关证据,经查实将立刻删除涉嫌侵权内容。

Like (0)
Donate 微信扫一扫 微信扫一扫
Previous 05/20 15:54
Next 05/20 15:54

相关推荐