最近在做一个需求,需要对不同类型slice做去重,比如去重的对象如下:
按下面这样:
这样去实现肯定是不妥的;
因为这种实现太繁琐,每增加一种类型都要增加一个函数,毫无通用性可言。
所以,通用且好用的去重函数是必然的选择。
千篇一律的答案在网上搜了一圈,基本都是类似下面的方案:
- 方案一:输入一个interface,函数体内不改变slice值,返回一个新的[]interface
- 方案二:输入的是具体的类型,函数体类改变输入的slice,没有返回值
两种方案都能一定成都上满足项目的需求,但:
方案一使用时还不太方便,还需要再把[]interface{} 转化成 具体类型的slice,
方案二则不具备通用性,每新增类型都需要再增加函数;
我期望的结果是,函数实现后,能按照如下两种方式供业务选择:
- 方式一:不改变输入的slice,输出的是一个interface
这样使用时就如下:
该方式省去了将[]interface转化为[]int64的逻辑,而直接用断言即可。
- 方式二:改变输入的slice,无返回值
该方式使用时则更方便了。
show me code上面两种方式我都实现了,先附上代码吧,有需要的同学可以直接拿去用。
方式一的实现
代码
使用
结果
符合预期。
方式二的实现
代码
为了减少代码篇幅,实现中调用Dedumplicate函数复用了方案一的去重逻辑。
使用
为了说明通用性,这次改为对[]string去重
结果
符合预期。
实现思路说实话,实现上面的两个效果,还真的破费我的精神的;
中间有问过一些golang比较熟的同学,貌似都没考虑过该问题,
所以一时也没能帮忙给出答案。
下面简单给出我的思考过程,也希望能增强自己对golang的认识。
具体类型的实现
通用往往是对个例的抽象,或者说是是归纳与演绎两大法宝之归纳法。
以对[]int64的去重为例:
小说明:这里的existMap其实就是充当set的作用。
利用反射实现方式一
怎样将上面的逻辑翻译成对下面通用interface的处理呢?
答案是:反射!
因为interface中保存着 运行时 原数据的类型和值,
而反射的特性用于处理运行时才知道类型的数据再合适不过了。
interface和reflect.Value的互转
- func ValueOf(i interface{}) Value
该函数可以获取到Interface{}实际存储的值; - func (v Value) Interface() (i interface{})
该函数可以将实际存储的值转化为interface{};
操作任意类型的slice
- reflect.MakeSlice
- reflect.Append
有了上面两个基础知识后,翻译也就水到渠成了。
从方式一到方式二
方式二修改原slice,节约空间的方法是用类似于quicksort的IN-PLACE算法,
但本文主要是探究语言层面的实现,因而对算法的优化有所忽略,
所以这里先调用方案一拿到去重后的结果,再修改原输入的slice。
怎么修改原slice,我还真的卡了好长时间!
注意点:slice的传参,是值传递!
所以要想修改原切片,传给参数的值必须是 指向切片的指针!
reflect.Value的两个重要函数
- func (v Value) Elem() Value
所以Elem()相当于*ptr的作用,也就是解引用。
- func (v Value) Set(x Value)
发散:从另一种实现看slice的内部结构
上面这种实现也是ok的。
因为slice实际上是下面这个结构:
喜欢的话,关注我的公众号哦image.png
本公众号希望从日常工作中的一个小点,深入浅出讲解golang、后台开发的知识点,欢迎一起探讨。