深入go泛型特性之comparable「附案例」

写作背景

如果你经常遇到一些操作,比如将 map 转换为 slice,判断一个字符串是否出现在 map 中,slice 中是否有重复元素等等,那你对下面这个库肯定不陌生。

github.com/samber/lo

最近抽业余时间在看了源码,底层用了范型封装并且用了预定义类型 comparable 作为约束,平时在开发中也会使用这个关键字封装基础操作,例如:

// 交集
func Intersect[T comparable](slice1, slice2 []T) []T {
	slice1Map := make(map[T]struct{})
	for _, s1 := range slice1 {
		slice1Map[s1] = struct{}{}
	}

	var ret []T
	for _, s2 := range slice2 {
		_, ok := slice1Map[s2]
		if ok {
			ret = append(ret, s2)
		}
	}

	return ret
}

// 求并集
func Union[T comparable](slices ...[]T) []T {
	elementMap := make(map[T]struct{})
	for _, sc := range slices {
		for _, element := range sc {
			elementMap[element] = struct{}{}
		}
	}

	retSlice := make([]T, 0, len(elementMap))
	for element := range elementMap {
		retSlice = append(retSlice, element)
	}
	return retSlice
}

// 去重
func Distinct[T comparable](arr []T) []T {
	if len(arr) == 0 {
		return arr
	}

	srcMap := make(map[T]struct{})
	for k := range arr {
		srcMap[arr[k]] = struct{}{}
	}

	var newArr []T
	for ar := range srcMap {
		newArr = append(newArr, ar)
	}
	return newArr
}

跟 lo 库做了一些类似操作,不过没有它丰富。当时在封装这部分代码时 lo 库还没有进入我们视野,于是造了一些轮子。

刚好看 GO 的发布 blog ,GO 团队对 comparable 进行了一些更新,于是决定研究一番这次升级了啥。

comparable 是在 GO 1.20 版本更新的,如果遇到一些编译问题,可能需要你升级下 SDK 版本(至少是 1.20 版本)。

名词解释

什么是 comparable

comparable 为 Go 中的预定义类型,comparable 是一种类型约束,用于指定某些类型可以进行相等性比较。这意味着我们可以使用 == 和 != 运算符来比较这些类型的值。

下面是官方 sdk 给出的解释。

// comparable is an interface that is implemented by all comparable types
// (booleans, numbers, strings, pointers, channels, arrays of comparable types,
// structs whose fields are all comparable types).
// The comparable interface may only be used as a type parameter constraint,
// not as the type of a variable.
type comparable interface{ comparable }

有几个关键信息

  1. "comparable"是一个接口,所有可比较类型实现这个接口;
  2. 包含了布尔型、数值型、字符串、指针、通道、结构体等,这里注意结构体中的所有成员变量都是可比较类型。
  3. 可比较的接口只能用作行参数约束,不可以作为变量的类型,这个应该比较好理解。

结构体中的所有成员变量都是可比较类型,可能不好理解。下面这段代码定义了 Person,Person 成员变量都是可比较的,所以编译通过并且能得到正确结果。

func Equal[P comparable](params1, params2 P) bool {
	return params1 != params2
}

type Person struct {
	Name string
	Age  int
}

Equal[Person](Person{
		Name: "11",
		Age:  0,
	}, Person{
		Name: "22",
		Age:  0,
	})

下面这段代码同样定义了 Person,但是有一个 Address 是 切片,切片是不可比较的,所以,Person 结构体是不可比较的,下面这段代码编译失败。

func Equal[P comparable](params1, params2 P) bool {
	return params1 != params2
}

type Person struct {
	Name    string
	Age     int
	Address []string
}

Equal[Person](Person{
		Name: "11",
		Age:  0,
	}, Person{
		Name: "22",
		Age:  0,
	})

编译器提示

Person does not satisfy comparable

看到这里,可能有同学要问了哪些类型支持比较呢?参考下面这个链接

https://go.dev/ref/spec#Comparison_operators​go.dev/ref/spec#Comparison_operators

可比较运算符

comparable 作为范型类型约束实参类型,实参类型一定是可比较类型,那可比较是啥意思?简单点说比较运算符比较两个数,并产生一个布尔值。

比较运算符有哪些呢?参考下面链接

The Go Programming Language Specification - The Go Programming Language

下面我贴一些关键信息。

==    equal
!=    not equal
<     less
<=    less or equal
>     greater
>=    greater or equal

比较运算符分为两大类:

1、 相等运算符 == 和 != 适用于可比较类型(comparable types)。

2、 有序类型的操作符 <、<=、> 和 >= 适用于有序类型(ordered types)。

comparable 和 ordered types 是不同的概念,如果类型约束是 comparable ,那么该类型只能使用 == 和 != 运算符,而不能使用排序运算符。

下面这段代码编译不通过的

func min[T comparable](x, y T) {
	if x > y {
		return
	}

	return
}

编译器提示,因为 comparable 不支持大小比较

Invalid operation: x > y (the operator > is not defined on T)

如果要支持大小比较可以改成下面这样

import (
	"fmt"
	"golang.org/x/exp/constraints"
)

func main() {
	fmt.Println(min[int](1, 2))
}

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

	return x
}

constraints.Ordered 约束支持的运算符有 <、<=、> 和 >=。

顺带解释下行参和实参

类型形参(type parameter)

函数的形参(parameter) 只是类似占位符并没有具体的值。

//  T 是形参,在定义函数时它的类型是不确定的,类似占位符
func min[T comparable](a T, b T) T {
	....
}

类型实参(type argument)

T 被称为类型形参(type parameter), 在函数定义时类型并不确定。因为类型不确定性,所以在调用函数的时候再传入具体的类型。被传入的具体类型被称为类型实参(type argument):

//  T 是形参,在定义函数时它的类型是不确定的,类似占位符
func min[T comparable](a T, b T) T {
	....
}

min[int](1,2)

min int,int 就是实参,含义就是把 min 函数定义的行参 T 替换为 int 类型,就如下面这段代码。

func min[T comparable](a int, b int) T {
	....
}

实例化:定义范型类型并不能直接使用,需要被实例化为实参才能使用。

type Map[K comparable, T any] struct {
	m map[K]T
}

func (t *Map[K, T]) Add(k K, val T) {
	if t.m == nil {
		t.m = make(map[K]T)
	}
	t.m[k] = val
}

func main() {
	customMap := new(Map[string, any])
	customMap.Add("123", 90)
	fmt.Println(customMap)
}

范型类型被实例化为 Map[string, any],结果输出:&{map[123:90]}。

comparable 诞生背景

== 和 != 运算符不仅可以支持在一些预定义的类型上,比如:int、 string、bool 等,还应该支持更多的类型,比如结构体、数组、interface。

再加上范型引入,在约束中列举所有这些类型是不可能的。所以需要用一种方式来让行参支持 == 和 != 。

为了解决这个问题,Go 1.18 引入了预定义类型 comparable,comparable 是一个接口类型,其类型集合是可比较类型的集合,并且在实参需要支持 == 或者 != 的情况下用作函数、类型约束。comparable 按照我的理解它是一个语法糖。

如果你尝试封装一些基础库,例如:判断 slice 中是否包含某一个值,通常会定义一些范型类型,为了保证代码的安全性,对传入的实参类型进行了约束( Tmp 为约束)。如下:

type Tmp interface {
	~int | ~string | ~float32 | ~float64 // ....后续可能持续增加
}

// 包含
func IsContain[T Tmp1](src []T, targets ...T) bool {
	if len(src) == 0 {
		return false
	}

	srcMap := make(map[T]struct{})
	for k := range src {
		srcMap[src[k]] = struct{}{}
	}

	for _, target := range targets {
		_, ok := srcMap[target]
		if !ok {
			return false
		}
	}
	return true
}

使用方代码

func main() {
	fmt.Println(IsContain[int]([]int{1, 2}, 2))
}

你可以思考下假设需求驱动,增加了一种类型,你需要在 Tmp 中增加一个约束,这种方式并不优雅。如果是上面类似场景,那你可以放心替换为 comparable了,不用写这么啰嗦的代码了。

说到这里,comparable 跟 interface/any 是有区别的。前者代表仅可比较类型,后者代表任何类型。简单点说就是 interface/any 的类型集是大于 comparable 的。

另外,在没有范型时,你们是否写过下面这样的代码?为了提高代码复用性,map 能兼容更多类型,所以把 map key 定义为 any,但是这种写法是不安全的。虽然编译器不会报错,但是当你运行下面这段代码时,会发生 Panic。

func main() {
	lookupMap := make(map[any]string)
	lookupMap[[]int{}] = "slice"
}
panic: runtime error: hash of unhashable type []int

goroutine 1 [running]:
main.main()

为啥会 Panic 呢?当动态类型存储在接口变量中的实际值的类型是不可比较时,就会在运行时发生 Panic。

相比 comparable,编译器就会提示你类型是否合法。我们把代码微调整下

type CustomMap[k comparable, v any] map[k]v

func main() {
	var lookupMap = make(CustomMap[[]string, string])
	lookupMap[1] = "2"
	fmt.Println(lookupMap)
}

上段代码,comparable 限制了类型是可比较的,当你传入 []int{} 作为 key 时,编辑器提示

Cannot use []string as the type comparable Type does not implement constraint 'comparable' because type is not comparable.

所以,comparable 优势还是非常明显的。

1.20 comparable 升级了什么

好了,下面该讲讲 GO 1.20 升级了啥,在 GO 1.20 版本前,comparable 是不允许你将行参实例化为 any 类型的。any 的类型集合比 comparable 的类型集合更大(不是它的子集),因此并不包含在 comparable 中。

func main() {
	var lookupMap = make(CustomMap[any, string])
	lookupMap[1] = "2"
	fmt.Println(lookupMap)
}

上段代码编译期就会提示

Cannot use any as the type comparable Basic interfaces satisfy 'comparable' type-checking rules starting with Go 1.20

那 Go 1.20 版本是如何解决这个问题的呢?将非严格可比较类型 any 包含在 comparable 类型集合中。

此时,依赖 comparable 的泛型函数不再具备静态类型安全性了,单个非可比较的值可能通过泛型函数或类型,导致 panic。举个例子,下面这段代码在编译期是无法检查异常的,在运行时会 Panic。

func main() {
	var lookupMap = make(CustomMap[any, string])
	lookupMap[[]string{}] = "2"
	fmt.Println(lookupMap)
}

想了解 comparable 升级详细背景可以看看这个:spec: allow basic interface types to instantiate comparable type parameters · Issue #56548 · golang/go · GitHub

comparable 使用场景

推荐大家仔细研读 lo 库,代码非常简单,它会帮助你了解更多使用场景,让你有种恍然大悟感觉。

github.com/samber/lo

总结

1. 在 GO 1.20 之前,某些可比较类型实际上未满足 comparable 约束,比如 any,所以 comparable 约束的行参是不允许你实例化为 any 的。

2. GO 1.20 之后更改了 comparable 的行为,使其包含所有可比较类型。另外,1.20 之后 comparable 不再是静态安全的了,如果使用不当也会导致 panic。

参考文献

https://go.dev/blog/comparable​go.dev/blog/comparable

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

c语言实现2048小游戏

#include <stdio.h> #include <stdlib.h> #include <time.h> #include <conio.h>int best 0 ;// 定义2048游戏的结构体 typedef struct { int martix[16]; // 当前4*4矩阵的数字 int martixPrior[16]; // 上一步的4*4矩阵的数字 int emptyIndex[16…

4.6(信息差)

&#x1f30d; 山西500千伏及以上输电线路工程首次采用无人机AI自主验收 &#x1f30b; 中国与泰国将开展国际月球科研站等航天合作 ✨ 网页版微软 PowerPoint 新特性&#xff1a;可直接修剪视频 &#x1f34e; 特斯拉开始在德国超级工厂生产出口到印度的右舵车 1.马斯克&…

Qt 使用QPropertyAnimation动画效果的图片浏览器

文章目录 效果图功能点代码解析图片切换显示与动画效果图片缩放 总结 效果图 功能点 加载指定路径下的所有图片并显示滑动滑动条查看指定图片&#xff0c;也滚轮切换图片滑动条缩略图加入动画效果图片可以进行缩放移动查看 代码解析 整体来说相对&#xff0c;显示图片的是一…

4.1 JavaScript的使用

JavaScript有两种使用方式&#xff1a;一是在HTML文档中直接添加代码&#xff1b;二是将JavaScript脚本代码写到外部的JavaScript文件中&#xff0c;再在HTML文档中引用该文件的路径地址。 这两种使用方式的效果完全相同&#xff0c;可以根据使用率和代码量选择相应的开发方式。…

【Qt】:常用控件(一:概述和QWidget核心属性)

常用控件 一.概述二.QWidget核心属性1.enabled&#xff08;是否可用&#xff09;2.geometry&#xff08;设置坐标&#xff09;3.WindTitle&#xff08;窗口标题&#xff09;4.windowIcon1.绝对路径2.qrc机制 5.windowOpacity&#xff08;透明度&#xff09; 一.概述 Widget是Q…

Spring源码解析-容器基本实现

spring源码解析 整体架构 defaultListableBeanFactory xmlBeanDefinitionReader 创建XmlBeanFactory 对资源文件进行加载–Resource 利用LoadBeandefinitions(resource)方法加载配置中的bean loadBeandefinitions加载步骤 doLoadBeanDefinition xml配置模式 validationMode 获…

SpringCloud学习(9)-GateWay网关-自定义拦截器

GateWay Filter详细配置说明 gateway Filter官网:Spring Cloud Gateway 作用&#xff1a; 请求鉴权异常处理记录接口调用时长统计 过滤器类别 全局默认过滤器&#xff1a;官网&#xff1a;Spring Cloud Gateway&#xff0c;出厂默认已有的&#xff0c;直接用&#xff0c;作…

【详细讲解0基础如何进入IT行业】

&#x1f308;个人主页: 程序员不想敲代码啊 &#x1f3c6;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f44d;点赞⭐评论⭐收藏 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共…

Deep Image Prior

自监督的开创性工作 从简单分布到复杂分布的映射&#xff0c;本质上是将重建限制到某一流形&#xff0c;在流形上通过观测图像的数据保真项作为监督。 称之为先验也是很准确&#xff0c;流形就是先验。 这个扰动也很关键&#xff0c;本质上一个平滑正则项。直观理解是各种扰动…

Redis从入门到精通(四)Redis实战(一)短信登录

文章目录 前言第4章 Redis实战4.1 短信登录4.1.1 基于session实现短信登录4.1.1.1 短信登录逻辑梳理4.1.1.2 创建测试项目4.1.1.3 实现发送短信验证码功能4.1.1.4 实现用户登录功能4.1.1.5 实现登录拦截功能4.1.1.6 session共享问题 4.1.2 基于Redis实现短信登录4.1.2.1 Key-Va…

MATLAB - 用命令行设计 MPC 控制器

系列文章目录 前言 本例演示如何通过命令行创建和测试模型预测控制器。 一、定义工厂模型 本示例使用《使用 MPC Designer 设计控制器》中描述的工厂模型。创建工厂的状态空间模型&#xff0c;并设置一些可选的模型属性&#xff0c;如输入、状态和输出变量的名称和单位。 % co…

(阿里云万网)-域名注册购买实名流程

1&#xff0c;进入阿里云网万官网 输入网址 https://wanwang.aliyun.com/?spm5176.161059.J_3207526240.33.581fa505OGhzsW 注册域名 &#xff0c;域名推荐com&#xff08;国际顶级域名&#xff09; &#xff0c;cn&#xff08;国内顶级域名&#xff09;。其中cn价钱比com便…

状态机高阶讲解-16

2534 01:44:41,942 --> 01:44:44,140 那我们&#xff0c;你看这里 2535 01:44:45,170 --> 01:44:46,452 你看这里改之后 2536 01:44:46,452 --> 01:44:48,833 它代码就做新的调整了嘛 2537 01:44:48,833 --> 01:44:49,200 对吧 2538 01:44:50,150 --> 01:44…

smbms:超市订单管理系统(项目分析)

smbms&#xff1a;超市订单管理系统&#xff08;项目分析&#xff09; 文章目录 smbms&#xff1a;超市订单管理系统&#xff08;项目分析&#xff09;前言一、项目介绍&#xff1a;二、项目来源&#xff1a;三、架构图&#xff1a;&#xff08;流程图&#xff09;四、使用了什…

【论文阅读】CompletionFormer:深度完成与卷积和视觉变压器

【论文阅读】CompletionFormer:深度完成与卷积和视觉变压器 文章目录 【论文阅读】CompletionFormer:深度完成与卷积和视觉变压器一、介绍二、联系工作深度完成Vision Transformer 三、方法四、实验结果 CompletionFormer: Depth Completion with Convolutions and Vision Tran…

腾讯云添加域名后不生效

问题原因 添加域名后不生效可能是因为没有加CDN域名解析 解决步骤

手动实现Tomcat底层机制+自己设计Servlet

文章目录 1.Tomcat整体架构分析自己理解 2.第一阶段1.实现功能2.代码1.TomcatV1.java 3.调试阶段1.阻塞在readLine导致无法返回结果 4.结果演示 3.第二阶段1.实现功能2.代码1.RequestHander.java2.TomcatV2.java 3.调试阶段1.发现每次按回车会接受到两次请求 4.结果演示 4.第三…

【Python使用】嘿马头条完整开发md笔记第4篇:数据库,1 方案选择【附代码文档】

嘿马头条项目从到完整开发笔记总结完整教程&#xff08;附代码资料&#xff09;主要内容讲述&#xff1a;课程简介&#xff0c;ToutiaoWeb虚拟机使用说明1 产品介绍,2 原型图与UI图,3 技术架构,4 开发,1 需求,2 注意事项。数据库&#xff0c;理解ORM1 简介,2 安装,3 数据库连接…

C++要点细细梳理(下)(内存分配、异常处理、template和文件读写)

4. 类动态内存分配 4.1 C语言动态内存分配&#xff1a;malloc和free 4.2 C动态内存分配&#xff1a;new和delete 思考&#xff1a;定义一个对象和定义一个普通变量有何区别? 普通变量:分配足够空间即可存放数据对象:除了需要空间&#xff0c;还要构造/析构 类比&#xff1a;…

关于 VScode, 点击文件右键或者在文件夹中没有 【 在vscode中打开选项】 解决办法

关于 VScode, 点击文件右键或者在文件夹中没有 【 在vscode中打开选项】 解决办法 段子手-168 2024-4-6 1、在任意位置创建一个文本文件。如&#xff1a;a.txt 2、复制以下代码到 a.txt 文本文件中。 &#xff08;注&#xff1a; 以 ; 开头的 , 是备注信息 , 不需要做任何修…