C# Dictionary源码剖析

时间:2023-03-08 20:15:09

参考:https://blog.****.net/exiaojiu/article/details/51252515

http://www.cnblogs.com/wangjun1234/p/3719635.html

源代码版本为 .NET Framework 4.6.1

Dictionary是Hashtable的一种泛型实现(也是一种哈希表)实现了IDictionary泛型接口和非泛型接口等,将键映射到相应的值。任何非 null 对象都可以用作键。使用与Hashtable不同的冲突解决方法,Dictionary使用拉链法。

概念重播

对于不同的关键字可能得到同一哈希地址,即key1 != key2 => F(key1)=F(fey2),这种现象叫做冲突,在一般情况下,冲突只能尽可能的少,而不能完全避免。因为,哈希函数是从关键字集合到地址集合的映像。通常,关键字集合比较大,它的元素包括很多有可能的关键字。既然如此,那么,如何处理冲突则是构造哈希表不可缺少的一个方面。

通常用于处理冲突的方法有:开放定址法、再哈希法、链地址法、建立一个公共溢出区等。

在哈希表上进行查找的过程和哈希造表的过程基本一致。给定K值,根据造表时设定的哈希函数求得哈希地址,若表中此位置没有记录,则查找不成功;否则比较关键字,若和给定值相等,则查找成功;否则根据处理冲突的方法寻找“下一地址”,只到哈希表中某个位置为空或者表中所填记录的关键字等于给定值时为止。

哈希函数

Dictionary使用的哈希函数是除留余数法,在源码中的公式为:

h = F(k) % m; m 为哈希表长度(这个长度一般为素数)

(内部有一个素数数组:3,7,11,17....如图:C# Dictionary源码剖析);

通过给定或默认的GetHashCode()函数计算出关键字的哈希码模以哈希表长度,计算出哈希地址。

拉链法

Dictionary使用的解决冲突方法是拉链法,又称链地址法。

拉链法的原理:将所有关键字为同义词的结点链接在同一个单链表中。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数 组T[0..m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。结构图大致如下:

C# Dictionary源码剖析

特别强调:拉链法只是使用链表的原理去解决冲突,并不是真的有一个链表存在。

基本成员

         private struct Entry {
public int hashCode; //31位散列值,32最高位表示符号位,-1表示未使用
public int next; //下一项的索引值,-1表示结尾
public TKey key; //键
public TValue value; //值
} private int[] buckets;//内部维护的数据地址
private Entry[] entries;//元素数组,用于维护哈希表中的数据
private int count;//元素数量
private int version;
private int freeList;//空闲的列表
private int freeCount;//空闲列表元素数量
private IEqualityComparer<TKey> comparer;//哈希表中的比较函数
private KeyCollection keys;//键集合
private ValueCollection values;//值集合
private Object _syncRoot;

buckets 就想在哈希函数与entries之间解耦的一层关系,哈希函数的F(k)变化不在直接影响到entries。 
freeList 类似一个单链表,用于存储被释放出来的空间即空链表,一般有被优先存入数据。 
freeCount 空链表的空位数量。

初始化函数 
该函数用于,初始化的数据构造

     private void Initialize(int capacity) {
//根据构造函数设定的初始容量,获取一个近似的素数
int size = HashHelpers.GetPrime(capacity);
buckets = new int[size];
for (int i = ; i < buckets.Length; i++) buckets[i] = -;
entries = new Entry[size];
freeList = -;
}

初始化Dictionary内部数组容器:buckets int[]和entries<T,V>[],分别分配长度3。

(内部有一个素数数组:3,7,11,17....如图:C# Dictionary源码剖析);

size 哈希表的长度是素数,可以使元素更均匀地分布在每个节点上。 
buckets 中的节点值,-1表示空值。 
freeList 为-1表示没有空链表。 
buckets 和 freeList 所值指向的数据其实全是存储于一块连续的内存空间(entries )之中。

插入元素

  public void Add(TKey key, TValue value) {
Insert(key, value, true);
}
private void Insert(TKey key, TValue value, bool add){
if( key == null ) {
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
} if (buckets == null) Initialize();
int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
int targetBucket = hashCode % buckets.Length; //循环冲突
for (int i = buckets[targetBucket]; i >= ; i = entries[i].next) {
if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) {
if (add) {
ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_AddingDuplicate);
}
entries[i].value = value;
version++;
return;
} collisionCount++;
} //添加元素
int index;
//是否有空列表
if (freeCount > ) {
index = freeList;
freeList = entries[index].next;
freeCount--;
}
else {
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; //单链接的节点数(冲突数)达到了一定的阈值,之后更新散列值
if(collisionCount > HashHelpers.HashCollisionThreshold && HashHelpers.IsWellKnownEqualityComparer(comparer))
{
comparer = (IEqualityComparer<TKey>) HashHelpers.GetRandomizedEqualityComparer(comparer);
Resize(entries.Length, true);
}
}
 

思路分析: 
(1)通过哈希函数寻址,计算出哈希地址(因为中间有一个解耦关系buckets,所以不再直接指向entries的索引值,而是buckets的索引)。 
(2)判断buckets中映射到的值是否为-1(即为空位)。若不为-1,表示有冲突,遍历冲突链,不允许重复的键。 
(3)判断是否有空链表,有则插入空链表的当前位置,将freeList指针后移,freeCount减一,否则将元素插入当前空位。在这一步,容量不足将自动扩容,若当前位置已经存在元素则将该元素的地址存在插入元素的next中,形成一个单链表的形式。类似下图中的索引0。 
C# Dictionary源码剖析

移除

  public bool Remove(TKey key) {
if(key == null) {
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
} if (buckets != null) {
int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
int bucket = hashCode % buckets.Length;
int last = -;//记录上一个节点
//定位到一个单链表,每一个节点都会保存下一个节点的地址,操作不再重新计算哈希地址
for (int i = buckets[bucket]; i >= ; last = i, i = entries[i].next) {
if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) {
if (last < ) {
buckets[bucket] = entries[i].next;
}
else {
entries[last].next = entries[i].next;
}
entries[i].hashCode = -;//移除的元素散列值值为-1
entries[i].next = freeList;//将移除的元素放入空列表
entries[i].key = default(TKey);
entries[i].value = default(TValue);
freeList = i;//记录当前地址,以便下个元素能直接插入
freeCount++;//空链表节点数+1
version++;
return true;
}
}
}
return false;
}

Dictionary中存储元素的结构非常有趣,通过一个数据桶buckets将哈希函数与数据数组进行了解耦,使得每一个buckets的值对应的都是一条单链表,在内存空间上却是连续的存储块。同时Dictionary在空间与性能之间做了一些取舍,消耗了空间,提升了性能(影响性能的最大因素是哈希函数)。 
移除思路分析: 
(1)通过哈希函数确定单链表的位置,然后进行遍历。 
(2)该索引对应的值为-1,表示没有没有单链接节点,返回false,结束 
(3)该索引对应的值大于-1,表示有单链表节点,进行遍历,对比散列值与key,将映射到的entries节点散列值赋-1,next指向空链表的第一个元素地址(-1为头节点),freeList指向头节点地址,空链表节点数+1,返回true,结束;否则返回false,结束。(此处的节点地址统指索引值)。

查询

  public bool TryGetValue(TKey key, out TValue value) {
int i = FindEntry(key);//关键方法
if (i >= ) {
value = entries[i].value;
return true;
}
value = default(TValue);
return false;
} private int FindEntry(TKey key) {
if( key == null) {
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
} if (buckets != null) {
int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
for (int i = buckets[hashCode % buckets.Length]; i >= ; i = entries[i].next) {
if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) return i;
}
}
return -;
}

代码是不是一目了然,在FindEntry方法中,定位单链接的位置,进行遍历,对比散列值与key,比较成功则返回true,结束。

扩容

   private void Resize() {
Resize(HashHelpers.ExpandPrime(count), false);
} private void Resize(int newSize, bool forceNewHashCodes) {
Contract.Assert(newSize >= entries.Length); //重新初始化一个比原来空间还要大2倍左右的buckets和Entries,用于接收原来的buckets和Entries的数据
int[] newBuckets = new int[newSize];
for (int i = ; i < newBuckets.Length; i++) newBuckets[i] = -;
Entry[] newEntries = new Entry[newSize]; //数据搬家
Array.Copy(entries, , newEntries, , count); //将散列值刷新,这是在某一个单链表节点数到达一个阈值(100)时触发
if(forceNewHashCodes) {
for (int i = ; i < count; i++) {
if(newEntries[i].hashCode != -) {
newEntries[i].hashCode = (comparer.GetHashCode(newEntries[i].key) & 0x7FFFFFFF);
}
}
} //单链表数据对齐,无关顺序
for (int i = ; i < count; i++) {
if (newEntries[i].hashCode >= ) {
int bucket = newEntries[i].hashCode % newSize;
newEntries[i].next = newBuckets[bucket];
newBuckets[bucket] = i;
}
}
buckets = newBuckets;
entries = newEntries;
}

foreach遍历 
Dictionary实现了IEnumerator接口,是可以用foreach进行遍历的,遍历的集合元素类型为KeyValuePair,是一种键值对的结构,实现是很简单的,包含了最基本的键属性和值属性, 
从代码中可以看出,用foreach遍历Dictionary就像用for遍历一个基础数组一样。 
这是内部类Enumerator(遍历就是对它进行的操作)中的方法MoveNext(实现IEnumerator接口的MoveNext方法)。

    public bool MoveNext() {
if (version != dictionary.version) {
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
} while ((uint)index < (uint)dictionary.count) {
if (dictionary.entries[index].hashCode >= ) {
current = new KeyValuePair<TKey, TValue>(dictionary.entries[index].key, dictionary.entries[index].value);
index++;
return true;
}
index++;
} index = dictionary.count + ;
current = new KeyValuePair<TKey, TValue>();
return false;
} public struct KeyValuePair<TKey, TValue> {
private TKey key;
private TValue value; public KeyValuePair(TKey key, TValue value) {
this.key = key;
this.value = value;
} //键属性
public TKey Key {
get { return key; }
} //值属性
public TValue Value {
get { return value; }
} public override string ToString() {
StringBuilder s = StringBuilderCache.Acquire();
s.Append('[');
if( Key != null) {
s.Append(Key.ToString());
}
s.Append(", ");
if( Value != null) {
s.Append(Value.ToString());
}
s.Append(']');
return StringBuilderCache.GetStringAndRelease(s);
}
}

Dictionary内部实现结构比Hashtable复杂,因为具有单链表的特性,效率也比Hashtable高。

举例说明:

一,实例化一个Dictionary, Dictionary<string,string> dic=new Dictionary<string,string>();

    a,调用Dictionary默认无参构造函数。

b,初始化Dictionary内部数组容器:buckets int[]和entries<T,V>[],分别分配长度3。(内部有一个素数数组:3,7,11,17....如图:C# Dictionary源码剖析);

  二,向dic添加一个值,dic.add("a","abc");

     a,将bucket数组和entries数组扩容3个长度。

b,计算"a"的哈希值,

c,然后与bucket数组长度(3)进行取模计算,假如结果为:2

d,因为a是第一次写入,则自动将a的值赋值到entriys[0]的key,同理将"abc"赋值给entriys[0].value,将上面b步骤的哈希值赋值给entriys[0].hashCode,

entriys[0].next 赋值为-1,hashCode赋值b步骤计算出来的哈希值。

e,在bucket[2]存储0。

三,通过key获取对应的value,  var v=dic["a"];

   a, 先计算"a"的哈希值,假如结果为2,

b,根据上一步骤结果,找到buckets数组索引为2上的值,假如该值为0.

c, 找到到entriys数组上索引为0的key,

1),如果该key值和输入的的“a”字符相同,则对应的value值就是需要查找的值。

2) ,如果该key值和输入的"a"字符不相同,说明发生了碰撞,这时获取对应的next值,根据next值定位buckets数组(buckets[next]),然后获取对应buckets上存储的值在定位到entriys数组上,......,一直到找到为止。

3),如果该key值和输入的"a"字符不相同并且对应的next值为-1,则说明Dictionary不包含字符“a”。

基本成员