一、Go语言实战——自定义集合Set
在Go语言中有作为Hash Table实现的字典(Map)类型,但标准数据类型中并没有集合(Set)这种数据类型。比较 Set 和 Map 的主要特性,有类似特性如下:
它们中的元素都是不可重复的。
它们都只能用迭代的方式取出其中的所有元素。
对它们中的元素进行迭代的顺序都是与元素插入顺序无关的,同时也不保证任何有序性。
但是,它们之间也有一些区别,如下:
Set 的元素是一个单一的值,而 Map 的元素则是一个键值对。
Set 的元素不可重复指的是不能存在任意两个单一值相等的情况。Map的元素不可重复指的是任意两个键值对中的键的值不能相等。
从上面的特性可知,可以把集合类型(Set)作为字典类型(Map)的一个简化版本。也就是说,可以用 Map 来编写一个 Set 类型的实现。实际上,在Java语言中,java.util.HashSet 类就是用 java.util.HashMap 类作为底层支持的。所以这里就从HashSet出发,逐步抽象出集合Set。
1. 定义HashSet
首先,在工作区的 src 目录的代码包 basic/set(可以自行定义,但后面要保持一致)中,创建一个名为 hash_set.go 的源码文件。
根据代码包 basic/set 可知,源码文件 hash_set.go 的包声明语句(关于这个一些规则可以看前面的系列博文)如下:
package set
上面提到可以将集合类型作为字典类型的一个简化版本。现在我们的 HashSet 就以字典类型作为其底层的实现。HashSet 声明如下:
type HashSet struct {
m map[interface{}]bool
}
</div>
如上声明 HashSet 类型中的唯一的字段的类型是 map[interface{}]bool。选择这样一个字典类型是因为通过将字典 m 的键类型设置为 interface{},让 HashSet 的元素可以是任何类型的,因为这里需要使用 m 的值中的键来存储 HashSet 类型的元素值。那使用 bool 类型作为 m 的值的元素类型的好处如下:
从值的存储形式的角度看,bool 类型值只占用一个字节。
从值的表示形式的角度看,bool 类型的值只有两个—true 和 false。并且,这两个值度都是预定义常量。
把 bool 类型作为值类型更有利于判断字典类型值中是否存在某个键。例如:如果在向 m 的值添加键值对的时候总是以 true 作为其中的元素的值,则索引表达式 m[“a”] 的结果值总能体现出在m的值中是否包含键为“a”的键值对。对于 map[interface{}]bool 类型的值来说,如下:
if m["a"] {// 判断是否m中包含键为“a”的键值对
//省略其他语句
}
</div>
如上 HashSet 类型的基本结构已确定了,现在考虑如何初始化 HashSet 类型值。由于字典类型的零值为 nil,而用 new 函数来创建一个 HashSet 类型值,也就是 new(HashSet).m 的求值结果将会是一个 nil (关于 new 函数可以查阅本人另一篇博文Go语言学习笔记5)。因此,这里需要编写一个专门用于创建和初始化 HashSet 类型值的函数,该函数声明如下:
func NewHashSet() *HashSet {
return &HashSet{m: make(map[interface{}]bool)}
}
</div>
如上可以看到,使用make函数对字段m进行了初始化。同时注意观察函数 NewHashSet 的结果声明的类型是 *HashSet 而不是 HashSet,目的是让这个结果值的方法集合中包含调用接收者类型为 HashSet 或 *HashSet 的所有方法。这样做的好处将在后面编写 Set 接口类型的时候再予以说明。
2.实现HashSet的基本功能
依据其他编程语言中的 HashSet 类型可知,它们大部分应该提供的基本功能如下:
添加元素值。
删除元素值。
清除所有元素值。
判断是否包含某个元素值。
获取元素值的数量。
判断与其他HashSet类型值是否相同。
获取所有元素值,即生成可迭代的快照。
获取自身的字符串表示形式。
现在对这些功能一一实现,读者可自行实现,以下仅供参考。
(1).添加元素值
//方法Add会返回一个bool类型的结果值,以表示添加元素值的操作是否成功。
//方法Add的声明中的接收者类型是*HashSet。
func (set *HashSet) Add(e interface{}) bool {
if !set.m[e] {//当前的m的值中还未包含以e的值为键的键值对
set.m[e] = true//将键为e(代表的值)、元素为true的键值对添加到m的值当中
return true //添加成功
}
return false //添加失败
}
</div>
这里使用 *HashSet 而不是 HashSet,主要是从节约内存空间的角度出发,分析如下:
当 Add 方法的接收者类型为 HashSet 的时候,对它的每一次调用都需要对当前 HashSet 类型值进行一次复制。虽然在 HashSet 类型中只有一个引用类型的字段,但是这也是一种开销。而且这里还没有考虑 HashSet 类型中的字段可能会变得更多的情况。
当 Add 方法的接收者类型为 *HashSet 的时候,对它进行调用时复制的当前 *HashSet 的类型值只是一个指针值。在大多数情况下,一个指针值占用的内存空间总会被它指向的那个其他类型的值所占用的内存空间小。无论一个指针值指向的那个其他类型值所需的内存空间有多么大,它所占用的内存空间总是不变的。
(2).删除元素值
//调用delete内建函数删除HashSet内部支持的字典值
func (set *HashSet) Remove(e interface{}) {
delete(set.m, e)//第一个参数为目标字典类型,第二个参数为要删除的那个键值对的键
}
</div>
(3).清除所有元素
//为HashSet中的字段m重新赋值
func (set *HashSet) Clear() {
set.m = make(map[interface{}]bool)
}
</div>
如果接收者类型是 HashSet,该方法中的赋值语句的作用只是为当前值的某个复制品中的字段m赋值而已,而当前值中的字段 m 则不会被重新赋值。方法 Clear 中的这条赋值语句被执行之后,当前的 HashSet 类型值中的元素就相当于被清空了。已经与字段 m 解除绑定的那个旧的字典值由于不再与任何程序实体存在绑定关系而成为了无用的数据。它会在之后的某一时刻被Go语言的垃圾回收器发现并回收。
(4).判断是否包含某个元素值。
//方法Contains用于判断其值是否包含某个元素值。
//这里判断结果得益于元素类型为bool的字段m
func (set *HashSet) Contains(e interface{}) bool {
return set.m[e]
}
</div>
当把一个 interface{} 类型值作为键添加到一个字典值的时候,Go语言会先获取这个 interface{} 类型值的实际类型(即动态类型),然后再使用与之对应的 hash 函数对该值进行 hash 运算,也就是说,interface{} 类型值总是能够被正确地计算出 hash 值。但是字典类型的键不能是函数类型、字典类型或切片类型,否则会引发一个运行时恐慌,并提示如下:
panic: runtime error: hash of unhashable type <某个函数类型、字典类型或切片类型的名称>
(5).获取元素值的数量。
//方法Len用于获取HashSet元素值数量
func (set *HashSet) Len() int {
return len(set.m)
}
</div>
(6).判断与其他HashSet类型值是否相同。
//方法Same用来判

