我们在《Core源码(一) ConcurrentDictionary》就对ConcurrentDictionary进行了解读,这篇我们一起看看普通的Dictionary如何实现。
在framework源码中的位置
既然要说 Dictionary就不得不提HashTable类,这里简单说下使用的区别:
(1).HashTable不支持泛型,而Dictionary支持泛型。
(2). Hashtable 的元素属于 Object 类型,所以在存储或检索值类型时通常发生装箱和拆箱的操作,所以你可能需要进行一些类型转换的操作,而且对于int,float这些值类型还需要进行装箱等操作,非常耗时。
(3).单线程程序中推荐使用 Dictionary, 有泛型优势, 且读取速度较快, 容量利用更充分。多线程程序中推荐使用 Hashtable, 默认的 Hashtable 允许单线程写入, 多线程读取, 对 Hashtable 进一步调用 Synchronized() 方法可以获得完全线程安全的类型. 而 Dictionary 非线程安全, 必须人为使用 lock 语句进行保护, 效率大减。(当然ConcurrentDictionary对多线程的支持也很好)
Dictionary是Hashtable的一种泛型实现(也是一种哈希表)实现了IDictionary泛型和非泛型接口等,将键映射到相应的值。任何非 null 对象都可以用作键。使用与Hashtable不同的冲突解决方法,Dictionary使用拉链法,而Hashtable再散列法解决冲突(双重散列的哈希函数)。
Dictionary内部实现结构比Hashtable复杂,因为具有单链表的特性,效率也比Hashtable高。内部维护了一个int数组对象buckets记录了每个求出的哈希值对应到数据数组的索引位置。哈希表拉链的实现是根据内部Entry对象数组的next属性实现的。
哈希函数
Dictionary使用的哈希函数是除留余数法,在源码中的公式为:
h = F(k) % m; m 为哈希表长度(这个长度一般为素数)
通过给定或默认的GetHashCode()函数计算出关键字的哈希码模以哈希表长度,计算出哈希地址。
对于不同的关键字可能得到同一哈希地址,即key1 != key2 => F(key1)=F(fey2),这种现象叫做冲突,在一般情况下,冲突只能尽可能的少,而不能完全避免。因为,哈希函数是从关键字集合到地址集合的映像。通常,关键字集合比较大,它的元素包括多有可能的关键字。既然如此,那么,如何处理冲突则是构造哈希表不可缺少的一个方面。
通常用于处理冲突的方法有:开放定址法、再哈希法、链地址法、建立一个公共溢出区等。
在哈希表上进行查找的过程和哈希造表的过程基本一致。给定K值,根据造表时设定的哈希函数求得哈希地址,若表中此位置没有记录,则查找不成功;否则比较关键字,若和给定值相等,则查找成功;否则根据处理冲突的方法寻找“下一地址”,知道哈希表中某个位置为空或者表中所填记录的关键字等于给定值时为止。
拉链法
Dictionary使用的解决冲突方法是拉链法,又称链地址法。
拉链法的原理:将所有关键字为同义词的结点链接在同一个单链表中。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数 组T[0..m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。结构图大致如下:
属性和变量
private struct Entry { public int hashCode; // Lower 31 bits of hash code, -1 if unused public int next; // Index of next entry, -1 if last public TKey key; // Key of entry public TValue value; // Value of entry } /// <summary> /// 内部维护的数据地址数组 我喜欢管他叫索引数组 /// </summary> private int[] buckets; /// <summary> /// Entry对象数组,实际存放数据的数组 /// </summary> private Entry[] entries; /// <summary> /// 元素数量 /// </summary> private int count; private int version; /// <summary> /// entries对象数组 空闲的位置 一般都指向最新的空闲位置。 /// </summary> private int freeList; /// <summary> /// entries对象数组 空闲的数量 /// </summary> private int freeCount; //哈希函数的提供者 如果构造函数没指定就使用默认的 private IEqualityComparer<TKey> comparer; private KeyCollection keys; private ValueCollection values; private object _syncRoot;
构造函数
常用的构造函数如下,内部调用Initialize
public MyDictionary(int capacity, IEqualityComparer<TKey> comparer) { if (capacity < 0) throw new ArgumentOutOfRangeException(); if (capacity > 0) Initialize(capacity); this.comparer = comparer ?? EqualityComparer<TKey>.Default; } private void Initialize(int capacity) { //和hashtable使用同样的capacity长度算法 int size = HashHelpers.GetPrime(capacity); buckets = new int[size]; //内部维护的地址数组buckets全部赋值-1 for (int i = 0; i < buckets.Length; i++) buckets[i] = -1; entries = new Entry[size]; freeList = -1; }
新增
private void Insert(TKey key, TValue value, bool add) { if (key == null) { throw new ArgumentNullException(); } if (buckets == null) Initialize(0); //获取哈希值 int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF; //根据code求目标位置 int targetBucket = hashCode % buckets.Length; // 使用内部维护的buckets数组,通过buckets[targetBucket]找到entries存放的位置,如果没有会返回-1 也就结束了这个循环 // 如果找到了,就从开始位置遍历这个拉链,看是否有相同的key,如果有就更新(有的情况继续新增会抛出异常) for (int i = buckets[targetBucket]; i >= 0; i = entries[i].next) { if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) { if (add) { throw new ArgumentException(); } entries[i].value = value; version++; return; } } int index; //没找到已有的key 就新增。这里先判断buckets这个地址记录的数组是否有空间 if (freeCount > 0) { //记录到空闲的位置上 index = freeList; //这个 freeList = entries[index].next; freeCount--; } else { //如果数量满了,就重新分配Resize if (count == entries.Length) { Resize(); targetBucket = hashCode % buckets.Length; } index = count; count++; } entries[index].hashCode = hashCode; entries[index].next = buckets[targetBucket]; entries[index].key = key; entries[index].value = value; //索引记录目前的对象位置 buckets[targetBucket] = index; version++; }
查询
private int FindEntry(TKey key) { if (key == null) { throw new ArgumentNullException(); } if (buckets != null) { int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF; //获取第一个索引的位置,然后遍历拉链 for (int i = buckets[hashCode % buckets.Length]; i >= 0; i = entries[i].next) { if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) return i; } } return -1; }
删除
public bool Remove(TKey key) { if (key == null) { throw new ArgumentNullException(); } if (buckets != null) { int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF; int bucket = hashCode % buckets.Length; int last = -1; for (int i = buckets[bucket]; i >= 0; last = i, i = entries[i].next) { if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) { if (last < 0) { //直接找到了,索引数组就短路掉目前这个元素,直接指向下一个 ,下一个可能是-1,也可能有值,这里就不管了。 buckets[bucket] = entries[i].next; } else { //对象数组 内部拉链短路掉要删除的元素,指向下一个。 entries[last].next = entries[i].next; } entries[i].hashCode = -1; entries[i].next = freeList; entries[i].key = default; entries[i].value = default; //指向对象数组最新的空闲位置。 freeList = i; freeCount++; version++; return true; } } } return false; }