深入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 []Tfor _, 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 []Tfor 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 stringAge  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    stringAge     intAddress []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 []intgoroutine 1 [running]:
main.main()

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

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

type CustomMap[k comparable, v any] map[k]vfunc 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

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

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

相关文章

云计算的安全需求

目录 一、概述 二、云安全服务基本能力要求 三、信息安全服务&#xff08;云计算安全类&#xff09;资质要求 3.1 概述 3.2 资质要求内容 3.2.1 组织与管理要求 3.2.2 技术能力要求 四、云安全主要合规要求 4.1 安全管理机构部门的建立 4.2 安全管理规范计划的编制 4…

【Flutter】Getx设计模式及Provider、Repository、Controller、View等

本文基于Getx 4,x 本本 1、引入 再次接触到Flutter项目&#xff0c;社区俨然很完善和活跃。pubs.dev 寻找状态管理的时候看到很熟悉的Getx时间&#xff0c;俨然发现Getx的版本已到是4.x版本&#xff0c;看到Getx的功能已经非常强大了&#xff0c;庞大的API俨然成为一种开发框架…

Windows Server 2008添加Web服务器(IIS)、WebDAV服务、网络负载均衡

一、Windows Server 2008添加Web服务器&#xff08;IIS&#xff09; &#xff08;1&#xff09;添加角色&#xff0c;搭建web服务器&#xff08;IIS&#xff09; &#xff08;2&#xff09;添加网站&#xff0c;关闭默认网页&#xff0c;添加默认文档 在客户端浏览器输入服务器…

蓝桥杯 十一届C++A组 字符排序 21分(运行超时)

思路&#xff1a; 1. 此题考查的冒泡排序中的交换次数&#xff0c;其实就是考察当前数与后面的逆序对个数问题。而为了最大利用位数&#xff0c;应当使每一位都不小于后面的字符&#xff0c;否则会造成一次逆序对的浪费&#xff08;贪心&#xff0c;为了使总位数最少&#xff…

每日OJ题_优先级队列_堆③_力扣692. 前K个高频单词

目录 力扣692. 前K个高频单词 解析代码 力扣692. 前K个高频单词 692. 前K个高频单词 难度 中等 给定一个单词列表 words 和一个整数 k &#xff0c;返回前 k 个出现次数最多的单词。 返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率&#xff0c…

《QT实用小工具·三》偏3D风格的异型窗体

1、概述 源码放在文章末尾 可以在窗体中点击鼠标左键进行图片切换&#xff0c;项目提供了一些图片素材&#xff0c;整体风格偏向于3D类型&#xff0c;也可以根据需求自己放置不同的图片。 下面是demo演示&#xff1a; 项目部分代码如下所示&#xff1a; 头文件部分&#xff…

基于SSM+Vue的服装商城系统

绪论 项目研究的背景 困扰管理层的许多问题当中,服装定制将是广大用户们不可忽视的一块。但是管理好服装定制又面临很多麻烦需要解决,例如,如何在工作琐碎,记录繁多的情况下将服装定制的当前情况反应给相关管理人员决策,等等。在此情况下开发一款服装定制系统&#xff0c;于是…

DataLoader的使用

DataLoader的使用 测试DataLoader&#xff0c;batch_size大小为4 import torchvision.datasets from torch.utils.data import DataLoadertest_data torchvision.datasets.CIFAR10("./dataset", trainFalse, transformtorchvision.transforms.ToTensor()) test_loa…

215 基于matlab的快速跟踪算法

基于matlab的快速跟踪算法&#xff0c;提出一种简单又快速、 鲁棒性的算法&#xff0c;基于贝叶斯框架下&#xff0c;该模型 &#xff08;即图像强度和从目标位置&#xff09; 的低级功能及周边地区的统计相关性的时空关系。跟踪问题是通过计算信心地图&#xff0c;并将以最大限…

闪站侠洗护管理系统,洗衣洗鞋小程序软件定制,干洗连锁店软件系统搭建;

闪站侠洗护管理系统&#xff0c;洗衣洗鞋小程序软件定制&#xff0c;干洗连锁店软件系统搭建&#xff1b; 为了让每一个洗衣洗鞋工厂与门店的连接更加高效便捷&#xff0c;送洗流程更加简单轻松&#xff0c;拽牛科技倾心打造洗衣洗鞋管理软件。我们的目标是通过高效和优质的服务…

Navicat Premium工具安装教程(超详细讲解)

Navicat Premium是一款功能强大并可支持多连接的数据库管理工具&#xff0c;它允许在单一程序中同时连接多达7种数据库&#xff0c;包括MySQL、MariaDB、MongoDB、SQL server、SQLite、Oracle和PostgreSQl数据库&#xff0c;让管理不同类型的数据库更加快速便捷。 安装Navicat…

隐私计算实训营学习九:隐语多方安全计算在安全核对的行业实践

文章目录 一、业务背景&#xff1a;安全核对产生的土壤二、产品方案&#xff1a;从试点到规模化的路三、技术共建&#xff1a;与隐语的共同成长 一、业务背景&#xff1a;安全核对产生的土壤 业务背景&#xff1a;很多粗放使用数据的方式被新出台的法律法规所规范&#xff0c;…

Redis的I/O多路复用

Redis是单线程的&#xff0c;为什么还那么快&#xff1f; 1.redis是基于内存的 2.redis使用I/O多路复用模型 关于I/O多路复用&#xff1a; 多路&#xff1a;多个客户端连接复用&#xff1a;使用单线程就能够实现同时处理多个客户端的连接 单线程去监控多个Socket&#xff…

数据库的简单查询

一、检索一列或多列1.检索单独一列 select 列名 from 表名; select order_num from orders; 2.检索多列数据 select 列 1&#xff0c;列 2... from 表名; select order_num,order_date from orders; select order_date,order_num from orders; 3.查询所有字段 select * from…

SQL注入---POST注入

文章目录 前言一、pandas是什么&#xff1f;二、使用步骤 1.引入库2.读入数据总结 一. POST提交概述 在Webshell文章中介绍过post提交和get提交的区别&#xff0c;这里不再赘述 post提交和get提交的区别&#xff1a; get方式提交URL中的参数信息&#xff0c;post方式则是将信…

博客部署001-centos安装docker

1、安装docker 1.1 卸载旧版本的 Docker sudo yum remove docker \docker-client \docker-client-latest \docker-common \docker-latest \docker-latest-logrotate \docker-logrotate \docker-engine1.2 设置 Docker 仓库 安装 Docker Engine 之前&#xff0c;首先需要设置…

幕译--本地字幕生成与翻译--Whisper客户端

幕译–本地字幕生成与翻译–Whisper客户端 本地离线的字幕生成与翻译&#xff0c;支持显卡加速。可免费试用&#xff0c;无次数限制 基于Whisper&#xff0c;希望做最好的Whisper客户端 功能介绍 本地离线&#xff0c;不用担心隐私问题支持显卡&#xff08;CUDA&#xff09;…

重读Java设计模式: 适配器模式解析

引言 在软件开发中&#xff0c;经常会遇到不同接口之间的兼容性问题。当需要使用一个已有的类&#xff0c;但其接口与我们所需的不兼容时&#xff0c;我们可以通过适配器模式来解决这一问题。适配器模式是一种结构型设计模式&#xff0c;它允许接口不兼容的类之间进行合作。本…

C++设计模式:装饰器模式(四)

1、定义与动机 装饰器模式定义&#xff1a;动态&#xff08;组合&#xff09;地给一个对象增加一些额外的职责。就增加功能而言&#xff0c;Decorator模式比生成子类&#xff08;继承&#xff09;更为灵活&#xff08;消除重复代码 & 减少子类个数&#xff09;。 在某些情…

c++的STL(8) -- queue

queue容器概述 queue容器实现了实现了和队列相同结构的容器。 如图&#xff0c;队列这种结构有两端: 队首和队尾。 对于队列&#xff0c;我们添加数据只能从队尾添加&#xff0c;删除数据只能从队首删除。是一种先进先出的结构。 -- 当然读取数据也只能从队首或者队尾读取。…