Equals和GetHashCode
在了解Hashset之前,先看看代码实现。在Hashset中,要用到两个函数:Equals和GetHashCode。因为Equals的实现,同时也要重写GetHashCode方法。
那么,为什么要重写GetHashCode函数呢?且看看官方警告:如果重写 GetHashCode 方法,您应重写 Equals, ,反之亦然。如果被重写 Equals 方法将返回 true 两个对象是否相等,被重写的测试时GetHashCode 方法必须返回两个对象相同的值。这里很明确说到,GetHashCode就是为了能让Equals相同的两个对象,有相同的Hash值。
Hashset存储原理
Hashset类中有多个成员变量,在存储中需要我们值得注意的就是 int[] buckets、Slot[] slots、int freeIndex、int lastIndex等,分别标记了对应哈希值的index、Hashset对象集合、空的位置和集合最后一个位置。其中,Slot是个结构体,包涵3个成员:int hashcode、T value、int next,分别是哈希值、对象和下一个(相同Hash值)对象的位置。
当我们查找一个对象,是否在Hashset中时,会利用该对象的GetHashCode,通过计算得到对应的bucket值(bucket存储插入操作最后一个对象在slot中的位置,稍后描述),然后再利用Equals来比较对象是否相等,不等则一直比较至Next=0为止。
所以说,Hashset是基于数组和链表管理的。
插入和删除
插入的时候,先查找集合中是否有相同变量(见存储原理),若没有,则开始插入新成员。首先看freeIndex是否有值(该值仅在删除操作后产生),若有就用这个位置,若没有,则用lastIndex(最后一个位置,用完后+1)。因而,找到了插入位置后,开始计算该slot的值:hashcode由GetHashCode产生,value就是新成员,而Next则是用HashCode计算对应位置中的buckets值(存储相同Hashcode值的对象在slots中的位置),并刷新bucket值为当前slot。因而我们知道,由于Next的计算机制,新插入的对象,会覆盖原来buckets的值。
删除操作,同插入一样,也要查找到对象。如果是bucket中第一个对象,在删除对象同时,bucket修改为该对象Next值,否则,则修改上一个对象的Next为该对象的Next(相当于将上个对象指针直接指向下一个对象)。然后,删除的slot值需要修正:hashcode=-1,value=default,next=freeList。所以我们知道,空位置也形成了一个链表。
所以,可以看出HashCode管理起来,也需要一点时间的。但是,由于插入和删除都用了数组+链表,故速度还是很快的。
遍历
遍历使用foreach。这里要说一点,遍历的顺序和插入的顺序可能会不相同(如,对插入第一个数据删除,再插入,则会得到该现象)。Hashset实现了IEnumerable接口,并通过version变量,确保数据在遍历中不发生变化(操作会改变version值)。
相信大家都很了解IEnumerable接口了吧。还有个泛型接口IEnumerable<T>。这里说下Current,在两个接口中都要实现,而非泛型接口Current是要用到的,需要完成“抛出异常”功能(索引超出则抛出异常),并调用了泛型接口Current实现。
好,回归遍历:MoveNext 就是对slot进行顺序遍历了,如果hashcode>=0,则说明输出有效。
总结
总的来说,Hashset通过GetHashCode值(hash相等才会再比较equal)和Equal方法确定是否有相同变量,没有则可以插入。存储是通过数组和链表结合而完成的。
如果学过了算法与数据结构应该很明白其原理了。当初我记得还有什么二次哈希算法来者的,反正好久没再接触了。
本人小白,本文章仅作学习总结,也作交流使用。。
如有错误,欢迎指点,谢谢!
插入操作的源代码
private bool AddIfNotPresent(T value)
{
int freeList;
if (this.m_buckets == null)
{
this.Initialize(0);
}
int hashCode = this.InternalGetHashCode(value);
int index = hashCode % this.m_buckets.Length;
int num3 = 0;
for (int i = this.m_buckets[hashCode % this.m_buckets.Length] - 1; i >= 0; i = this.m_slots[i].next)
{
if ((this.m_slots[i].hashCode == hashCode) && this.m_comparer.Equals(this.m_slots[i].value, value))
{
return false;
}
num3++;
}
if (this.m_freeList >= 0)
{
freeList = this.m_freeList;
this.m_freeList = this.m_slots[freeList].next;
}
else
{
if (this.m_lastIndex == this.m_slots.Length)
{
this.IncreaseCapacity();
index = hashCode % this.m_buckets.Length;
}
freeList = this.m_lastIndex;
this.m_lastIndex++;
}
this.m_slots[freeList].hashCode = hashCode;
this.m_slots[freeList].value = value;
this.m_slots[freeList].next = this.m_buckets[index] - 1;
this.m_buckets[index] = freeList + 1;
this.m_count++;
this.m_version++;
if ((num3 > 100) && HashHelpers.IsWellKnownEqualityComparer(this.m_comparer))
{
this.m_comparer = (IEqualityComparer<T>) HashHelpers.GetRandomizedEqualityComparer(this.m_comparer);
this.SetCapacity(this.m_buckets.Length, true);
}
return true;
}