Go泛型详解

引子

如果我们要写一个函数分别比较2个整数和浮点数的大小,我们就要写2个函数。如下:

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

2个函数,除了数据类型不一样,其他处理逻辑完全一言。那有没有方法能一个函数完成上面的功能呢?有,那就是泛型。

func min2[T int | float64](x, y T) T {if x < y {return x}return y
}

泛型

官网文档:https://go.dev/blog/intro-generics
泛型为该语言添加了三个新的重要功能:

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

类型参数(Type Parameters)

现在允许函数和类型具有类型参数。类型参数列表看起来与普通参数列表类似,只是它使用方括号而不是圆括号。
在这里插入图片描述

package mainimport ("fmt""golang.org/x/exp/constraints"
)func GMin[T constraints.Ordered](x, y T) T {if x < y {return x}return y
}func main() {x := GMin[int](2, 3)fmt.Println(x) // 输出结果为2
}

其中constraints.Ordered是自定义类型(这里不展示源码)。
理解不了的,可以暂时把constraints.Ordered替换为 int | float64

向 GMin 提供类型参数(在本例中为 int)称为实例化(instantiation)。实例化分两步进行。

  • 首先,编译器在整个泛型函数或类型中将所有类型实参替换为其各自的类型参数。
  • 其次,编译器验证每个类型参数是否满足各自的约束。

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

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

全部代码为

package mainimport ("fmt""golang.org/x/exp/constraints"
)func GMin[T constraints.Ordered](x, y T) T {if x < y {return x}return y
}func main() {fmin := GMin[float64] // 相当于func GMin(x, y float64) float64{...}m := fmin(2.71, 3.14)fmt.Println(m) // 输出结果为2.71
}

实例化 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] 是使用类型参数 string 实例化 Tree 的示例。

类型集(Type sets)

类型参数列表中的每个类型参数都有一个类型。由于类型参数本身就是一种类型,因此类型参数的类型定义了类型集。这种元类型称为类型约束

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

在 Go 中,类型约束必须是接口。也就是说,接口类型可以用作值类型(value type),也可以用作元类型(meta-type)。

  1. 接口作为值类型:

当接口用作值类型时,它定义了一组方法,任何实现了这些方法的类型都可以赋值给这个接口变量。这是接口最常见的用法。

例如:

type Stringer interface {String() string
}type Person struct {Name string
}func (p Person) String() string {return p.Name
}var s Stringer = Person{"Alice"} // Person 实现了 Stringer 接口
fmt.Println(s.String()) // 输出: Alice

在这个例子中,Stringer 接口被用作值类型,Person 类型实现了 String() 方法,因此可以赋值给 Stringer 类型的变量。

  1. 接口作为元类型(meta-type):

当接口用作元类型时,它定义了一组类型约束,用于泛型编程。

例如:

type Ordered interface {int | float64 | string
}func Min[T Ordered](a, b T) T {if a < b {return a}return b
}fmt.Println(Min(3, 5))       // 输出: 3
fmt.Println(Min(3.14, 2.71)) // 输出: 2.71
fmt.Println(Min("a", "b"))   // 输出: a

在这个例子中,Ordered 接口被用作元类型,它定义了一组可以进行比较操作的类型(整数、浮点数和字符串)。Min 函数使用这个接口作为类型约束,可以接受任何满足 Ordered 约束的类型作为参数。

它们不仅可以定义对象的行为(作为值类型),还可以定义类型集合(作为元类型),从而在保持语言简洁性的同时,大大增强了代码的表达能力和复用性。

直到最近,Go 规范还说接口定义了一个方法集,大致就是接口中枚举的方法集。任何实现所有这些方法的类型都实现该接口。
在这里插入图片描述
但看待这个问题的另一种方式是说接口定义了一组类型,即实现这些方法的类型。从这个角度来看,作为接口类型集元素的任何类型都实现该接口。
在这里插入图片描述
这两种视图导致相同的结果:对于每组方法,我们可以想象实现这些方法的相应类型集,即由接口定义的类型集。

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

我们扩展了接口类型的语法来实现这一点。例如,interface{ int|string|bool } 定义了包含 int、string 和 bool 类型的类型集。
在这里插入图片描述
另一种说法是,该接口仅由 int、string 或 bool 满足。

现在让我们看看constraints.Ordered的实际定义:

type Ordered interface {Integer|Float|~string
}

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

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

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

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

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

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

因为这是常见的情况,所以对于约束位置的接口,可以省略封闭的interface{},我们可以简单地编写(Go 语言中泛型的语法糖和类型约束的简化写法):

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

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

[S ~[]E, E any]

在使用类型约束时,如果省略了外层的interface{}会引起歧义,那么就不能省略。例如:

type IntPtrSlice1 [T * int][]T             // 错误,这里会把*误会为乘号
type IntPtrSlice2[T *int,] []T             // 正确,只有一个类型约束时可以添加`,`
type IntPtrSlice3[T interface{ *int }] []T // 正确,使用interface{}包裹
type IntPtrSlice4[T *int, T2 *int] []T     // 正确

只有IntPtrSlice1是语法错误的,IntPtrSlice2-4语法正确

类型推断(Type inference)

函数参数类型推断

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

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

类型参数 T 用于指定普通非类型参数 x 和 y 的类型。正如我们之前看到的,可以使用显式类型参数来调用它

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

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

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

这是通过将参数 a 和 b 的类型与参数 x 和 y 的类型进行匹配来实现的。
这种从函数参数的类型推断出参数类型的推断称为函数参数类型推断

约束类型推断

该语言支持另一种类型推断,即约束类型推断。为了描述这一点,让我们从缩放整数切片的示例开始:

// 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 []int32func (p Point) String() string {// Details not important.
}

有时我们想要缩放一个点。由于 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) 的错误。

完整代码见:https://gitee.com/qingfeng-169/blog-code/blob/master/csdn/generics/demo1/main.go

问题在于Scale(p, 2)相当于Scale[int32](p, 2),Scale 函数返回 []E 类型的值,其中 E 是参数切片的元素类型(这里是int32)。当我们使用 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,它是切片参数的类型。我们对其进行了约束,使基础类型为 S 而不是 []E,结果类型现在为 S。由于 E 被约束为整数,因此效果与之前相同:第一个参数必须是某种整数类型的切片。函数体的唯一变化是,现在当我们调用 make 时,我们传递 S,而不是 []E。

有助于理解的代码:https://gitee.com/qingfeng-169/blog-code/blob/master/csdn/generics/demo2/main.go
完整代码见:https://gitee.com/qingfeng-169/blog-code/blob/master/csdn/generics/demo3/main.go
有助于理解的代码:https://gitee.com/qingfeng-169/blog-code/blob/master/csdn/generics/demo4/main.go

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

约束类型推断从类型参数约束中推导出类型实参。当一个类型参数具有根据另一类型参数定义的约束时使用它。当这些类型参数之一的类型参数已知时,约束用于推断另一个类型参数的类型参数。

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vue实现a-model弹窗拖拽移动

通过自定义拖拽指令实现 实现效果 拖动顶部&#xff0c;可对整个弹窗实施拖拽&#xff08;如果需要拖动底部、中间内容实现拖拽&#xff0c;把下面的ant-modal-header对应改掉就行&#xff09; 代码实现 编写自定义指令 新建一个ts / js文件&#xff0c;用ts举例 import V…

前端的页面代码

根据老师教的前端页面的知识&#xff0c;加上我也是借鉴了老师上课所说的代码&#xff0c;马马虎虎的写出了页面。如下代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</ti…

Databricks 收购 Tabular 的意义:数据开放框架的胜利

Databricks 宣布收购 Tabular&#xff0c;这是一个由 Apache Iceberg 的原始创建者开发的数据平台&#xff0c;在数据分析行业引发了涟漪。此次收购凸显了开放框架在数据领域日益增长的重要性&#xff0c;预示着数据管理、分析和 AI/ML 计划领域的创新、协作和可访问性的新时代…

QT实现自定义带有提示信息的透明环形进度条

1. 概述 做界面开发的童鞋可能都会遇到这样的需求&#xff0c;就是有一些界面点击了之后比较耗时的操作&#xff0c;需要界面给出一个环形进度条的进度反馈信息. 如何来实现这样的需求呢&#xff0c;话不多说&#xff0c;上效果 透明进度条 2. 代码实现 waitfeedbackprogressba…

如何在 CentOS 上配置本地 YUM 源

引言 CentOS 作为一个流行的企业级 Linux 发行版&#xff0c;依赖 YUM&#xff08;Yellowdog Updater, Modified&#xff09;来管理软件包。YUM 源&#xff08;Repository&#xff09;是软件包存储和分发的中心&#xff0c;它们通常位于互联网上。然而&#xff0c;在某些情况下…

科技与水利的完美融合:从数据采集到智能决策,全面解析智慧水利解决方案如何助力水利行业实现智能化管理

本文关键词&#xff1a;智慧水利、智慧水利工程、智慧水利发展前景、智慧水利技术、智慧水利信息化系统、智慧水利解决方案、数字水利和智慧水利、数字水利工程、数字水利建设、数字水利概念、人水和协、智慧水库、智慧水库管理平台、智慧水库建设方案、智慧水库解决方案、智慧…

c++ 多边形 xyz 数据 获取 中心点方法,线的中心点取中心值搞定 已解决

有需求需要对。多边形 获取中心点方法&#xff0c;绝大多数都是 puthon和java版本。立体几何学中的知识。 封装函数 point ##########::getCenterOfGravity(std::vector<point> polygon) {if (polygon.size() < 2)return point();auto Area [](point p0, point p1, p…

nodejs模板引擎(一)

在 Node.js 中使用模板引擎可以让您更轻松地生成动态 HTML 页面&#xff0c;通过将静态模板与动态数据结合&#xff0c;您可以创建可维护且易于扩展的 Web 应用程序。以下是一个使用 Express 框架和 EJS 模板引擎的基本示例&#xff1a; 安装必要的依赖&#xff1a; 首先&#…

mybatilsplaus 常用注解

官网地址 baomidou注解配置

医疗器械FDA |FDA网络安全测试具体内容

医疗器械FDA网络安全测试的具体内容涵盖了多个方面&#xff0c;以确保医疗器械在网络环境中的安全性和合规性。以下是根据权威来源归纳的FDA网络安全测试的具体内容&#xff1a; 一、技术文件审查 网络安全计划&#xff1a;制造商需要提交网络安全计划&#xff0c;详细描述产…

7-1、2、3 IPFS介绍使用及浏览器交互(react+区块链实战)

7-1、2、3 IPFS介绍使用及浏览器交互&#xff08;react区块链实战&#xff09; 7-1 ipfs介绍7-2 IPFS-desktop使用7-3 reactipfs-api浏览器和ipfs交互 7-1 ipfs介绍 IPFS区块链上的文件系统 https://ipfs.io/ 这个网站本身是需要科学上网的 Ipfs是点对点的分布式系统 无限…

深入Linux:权限管理与常用命令详解

文章目录 ❤️Linux常用指令&#x1fa77;zip/unzip指令&#x1fa77;tar指令&#x1fa77;bc指令&#x1fa77;uname指令&#x1fa77;shutdown指令 ❤️shell命令以及原理❤️什么是 Shell 命令❤️Linux权限管理的概念❤️Linux权限管理&#x1fa77;文件访问者的分类&#…

【香橙派 Orange pi AIpro】| 开发板深入使用体验

目录 一. &#x1f981; 写在前面二. &#x1f981; 愉快的安装流程2.1 安装前准备2.2 流程准备2.2.1 烧录镜像2.2.2 开机2.2.3 连网2.2.4 SSH远程连接开发板 2.3 体验 AI 应用样例 三. &#x1f981; 写在最后 一. &#x1f981; 写在前面 大家好&#xff0c;我是狮子呀&…

react 组件通信 —— 父子传值 【 函数式/类式 】

1、函数式组件通信 父子间通信 —— 父传子 父组件 export default function father() {return (<div style{{width:400px,height:200px,background:pink,marginLeft:500px}}>我是父组件<hr /><Son name{"韩小刀"}/></div>) } 子组件 ex…

数据结构--二叉树相关习题5(判断二叉树是否是完全二叉树 )

1.判断二叉树是否是完全二叉树 辨别&#xff1a; 不能使用递归或者算节点个数和高度来判断。 满二叉树可以用高度和节点来判断&#xff0c;因为是完整的。 但是完全二叉树前面是满的&#xff0c;但是最后一层是从左到右连续这种 如果仍然用这种方法的话&#xff0c;如下图…

代码随想录二刷7.22|977.有序数组的平方

暴力解法&#xff1a; ——如果想暴力解决这个问题的话&#xff0c;可以像题目那样&#xff0c;先将每一个元素平方&#xff0c;然后再排序 双指针&#xff1a; ——从题目中找到的信息&#xff1a;这是一个非递减顺序的整数数组&#xff0c;从例子中&#xff0c;可以容易看…

STM32 - SPI硬件外设

配合我的上一篇SPI ​​​​​​通信 协议-CSDN博客一起理解更佳&#xff0c;本文后看 SPI 是由摩托罗拉(Motorola)公司开发的全双工同步串行总线&#xff0c;是 MCU 和外围设备之间进行通信的同步串行端口。主要应用在EEPROM、Flash、RTC、ADC、网络控制器、MCU、DSP以及数字信…

PostgreSQL 中如何处理数据的批量更新和事务日志管理?

文章目录 PostgreSQL 中数据的批量更新和事务日志管理 PostgreSQL 中数据的批量更新和事务日志管理 在数据库的世界里&#xff0c;数据的批量更新和事务日志管理就像是一场精心编排的舞蹈&#xff0c;需要精准的步伐和协调的动作。对于 PostgreSQL 而言&#xff0c;这两个方面…

数学建模美赛经验小结

图片资料来自网络所听讲座&#xff0c;感谢分享&#xff01;

用Qwt进行图表和数据可视化开发

目录 Qwt介绍 示例应用场景 典型QWT开发流程 举一些Qwt的例子&#xff0c;多绘制几种类型的图像 1. 绘制折线图 (Line Plot) 2. 绘制散点图 (Scatter Plot) 3. 绘制柱状图 (Bar Plot) 4. 绘制直方图 (Histogram) Qwt介绍 QWT开发主要涉及使用QWT库进行图表和数据可视化…