C# Dictionary源码解析

Dictionary底层原理

本篇文章将介绍C#在.NET下的Dictionary的底层源码,源码都根据自己的理解加上了注释,源码直接到官网即可查看下载https://referencesource.microsoft.com/#mscorlib/system/collections/generic/dictionary.cs

关键数据结构 Entry

private struct Entry {
    
    
     public int hashCode;    // 哈希值,-1代表此Entry未使用
     public int next;        // 下一个Entry的索引,-1说明此Entry为末尾
     public TKey key;        // Key of entry
     public TValue value;    // Value of entry
}

重要变量

private int[] buckets; // Hash桶,存储Entry下标
private Entry[] entries; // Entry数组,存放元素
private int count; // Entries当前的下标
private int version; // 当前版本,防止迭代过程中集合被更改
private int freeList; // 被Remove的Entry的头节点下标
private int freeCount; // 有多少个被删除的Entry,有多少个空闲的位置
private IEqualityComparer<TKey> comparer; // 比较器
private KeyCollection keys; // 存放Key的集合
private ValueCollection values; // 存放Value的集合

初始化 Initialize

private void Initialize(int capacity) {
    
    
    //buckets和entries的size为大于容量的最小质数
    int size = HashHelpers.GetPrime(capacity);
    buckets = new int[size];
   	//初始所有桶都没有记录 值为-1
    for (int i = 0; i < buckets.Length; i++) buckets[i] = -1;
    entries = new Entry[size];
    freeList = -1;
}
  • 注意点:Hash桶的容量为什么要大于容量的最小质数
    • Hash桶的容量大于等于给定的容量无可厚非
    • Hash桶的容量必须为质数,原因跟哈希函数密切相关,如果哈希函数选择的好对容量也就没有这么多限制,比较常见的哈希就是将哈希值与桶容量取余,涉及到除法就有可能为整除,加入桶容量为15 ,某关键字和15取余映射到0,且其步长为5,如果0 5 10三个位置都已经被占用那么其只会不停的0 5 10 0 5 10 … 0 5 10,会造成死循环程序的崩溃。

增加Add 和 修改

Dictionary提供了公开Add方法,同时也提供了对操作符 [ ] 的重载,下面是两者的不同

  • Add只能进行增加,如果主键重复会进行报错,方法内部调用的是Insert(key, value, true);
  • 操作符[ ]可增加,可修改,主键重复即是修改,方法内部调用的是Insert(key, value, false);
private void Insert(TKey key, TValue value, bool add) {
    
    
    if( key == null ) {
    
    
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
    }
    if (buckets == null) Initialize(0);
    int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
    int targetBucket = hashCode % buckets.Length;
    //这一步是为了判断是否需要修改
    //根据哈希值找到对应的桶,进而找到桶记录的相应的Entry下标,由于可能哈希冲突所以要循环
    for (int i = buckets[targetBucket]; i >= 0; i = entries[i].next) {
    
    
        if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) {
    
    
             //已经存在,若是add调用则抛出错误,否则进行修改并返回即可
             if (add) {
    
     
                ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_AddingDuplicate);
             }
             entries[i].value = value;
             version++;
             return;
        } 
    }
    //接下来的逻辑是不存在时,应该将其赋给一个空闲的Entry,并做相应的哈希运算
    //index为将要分配给新Entry在Entries中的位置
    int index;
    //freeList记录着被删除的Entry链表的头节点索引,freeCount是个数,优先将新Entry分配到之前被删除的Entry位置
    if (freeCount > 0) {
    
    
        index = freeList;
        freeList = entries[index].next;
        freeCount--;
    }
    else {
    
    
        //当freeList没有空闲位置时,则应继续在Entries数组的末尾添加新Entry,注意容量不够时需要先扩容
        if (count == entries.Length)
        {
    
    
            Resize();
            //扩容后需要重新计算哈希值
            targetBucket = hashCode % buckets.Length;
        }
        index = count;
        count++;
   }
   //将Entry的值复制到entries[index]下,采用头插法维护buckets对应的链表
   entries[index].hashCode = hashCode;
   entries[index].next = buckets[targetBucket]; //头插法,让新node指向原链表的头
   entries[index].key = key;
   entries[index].value = value;
   buckets[targetBucket] = index; //头插法,新node成为新链表的头
   version++;
    
    //collisionCount,如果哈希碰撞次数过多,也会进行扩容,此时扩容并不真正的扩大容量而是重新进行哈希值的计算,重新构造Bucket,以减少冲突次数
    if(collisionCount > HashHelpers.HashCollisionThreshold && HashHelpers.IsWellKnownEqualityComparer(comparer)) 
    {
    
    
       	comparer = (IEqualityComparer<TKey>) HashHelpers.GetRandomizedEqualityComparer(comparer);
   		Resize(entries.Length, true);
    }
}
  • 注意点
    • 无论是哈希冲突所构造链表,还是Remove构造freeList空闲链表,都采用的头插法
    • 注意添加新元素时,应该优先填补因Remove而加入freeList的元素而不是继续向末尾添加
      • 这种设计使得count指针不会回溯,也不会每次遍历找空位置,以空间换时间的一种巧妙思想
  • 设计妙处
    • Entry连续的存在Entries数组中,所有用到的链表都是利用数组的下标模拟实现的
      • 减少了内存碎片化,申请一块大内存而避免频繁多次申请小内存串成链表

删除Remove

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 = -1; //置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)) {
    
    
                //当last == -1说明要删除的是头节点,直接将头指针指向next即可,否则将last对应的next指向当前i的next即可
             		if (last < 0) {
    
    
                		buckets[bucket] = entries[i].next;
      				}
                	else {
    
    
                    	entries[last].next = entries[i].next;
                	}
                //将Remove的Entry置为初始状态,并利用头插法加入到freeList中
                	entries[i].hashCode = -1;
                	entries[i].next = freeList;
                	entries[i].key = default(TKey);
                	entries[i].value = default(TValue);
               		freeList = i;
              		freeCount++;
              		version++;
                	return true;
     		}
   		}
    }
    return false;
}

查询 FindEntry

查询相对较为简单,一般通过 操作符[ ]即可实现访问,但这种查询方式不够安全当主键不存在时会报错,使用TryGetValue即可防止主键不存在时的报错。

  • [ ]方式,若不存在则抛出错误

    • public TValue this[TKey key] {
              
              
         	get {
              
              
          	int i = FindEntry(key);
              if (i >= 0) return entries[i].value;
             	ThrowHelper.ThrowKeyNotFoundException();
                	return default(TValue);
              }
             	set {
              
              
                 	Insert(key, value, false);
              }
      }
      
  • TryGetValue方式,若不存在则返回false

    • public bool TryGetValue(TKey key, out TValue value) {
              
              
      	int i = FindEntry(key);
      	if (i >= 0) {
              
              
      		value = entries[i].value;
      			return true;
      	}
      	value = default(TValue);
      	return false;
      }
      

查询就是根据哈希定位对应的桶,根据桶中记录的下标找到Entries对应的Entry因为哈希冲突的存在还要遍历链表

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 >= 0; i = entries[i].next) {
    
    
			if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) return i;
		}
	}
	return -1;
}

扩容操作 Resize

  • 扩容操作的触发条件

    • Entries已经满了,此时必须要进行扩容操作。

      • private void Resize() {
                  
                  
        	Resize(HashHelpers.ExpandPrime(count), false);
        }
        // 返回大于两倍的oldsize的最小质数,如果超出了所定义的最大值则取最大值
        public static int ExpandPrime(int oldSize)
        {
                  
                  
        	int newSize = 2 * oldSize;
         
        	// Allow the hashtables to grow to maximum possible size (~2G elements) before encoutering capacity overflow.
        	// Note that this check works even when _items.Length overflowed thanks to the (uint) cast
        	if ((uint)newSize > MaxPrimeArrayLength && MaxPrimeArrayLength > oldSize)
        	{
                  
                  
                //保证最大值本身就是质数
        		Contract.Assert( MaxPrimeArrayLength == GetPrime(MaxPrimeArrayLength), "Invalid MaxPrimeArrayLength");
        		return MaxPrimeArrayLength;
        	}
         
        	return GetPrime(newSize);
        }
        
    • CollisionCount哈希碰撞次数过多,也会进行扩容操作(此时的扩容并非真正的扩大容量,而是重新计算哈希值,重新构造Bucket和Entries已减少哈希冲突。

      • Resize(entries.Length, true);  //容量不改变,重新计算哈希
        

Resize方法

private void Resize(int newSize, bool forceNewHashCodes) {
    
    
    //保证新size要大于等于oldsize
	Contract.Assert(newSize >= entries.Length);
    //用新容量创建两个新副本
	int[] newBuckets = new int[newSize];
	for (int i = 0; i < newBuckets.Length; i++) newBuckets[i] = -1;
	Entry[] newEntries = new Entry[newSize];
    //将原Entries中的复制到副本中
	Array.Copy(entries, 0, newEntries, 0, count);
    //如果是哈希冲突次数过多调用的则重新计算哈希值
	if(forceNewHashCodes) {
    
    
		for (int i = 0; i < count; i++) {
    
    
			if(newEntries[i].hashCode != -1) {
    
    
					newEntries[i].hashCode = (comparer.GetHashCode(newEntries[i].key) & 0x7FFFFFFF);
			}
		}
	}
    //由于size的改变要重新构造bucket,注意可能因为Remove造成的Entries中间部分为空,要略过
	for (int i = 0; i < count; i++) {
    
    
		if (newEntries[i].hashCode >= 0) {
    
    
			int bucket = newEntries[i].hashCode % newSize;
			newEntries[i].next = newBuckets[bucket];
			newBuckets[bucket] = i;
		}
	}
	buckets = newBuckets;
	entries = newEntries;
}

猜你喜欢

转载自blog.csdn.net/Q540670228/article/details/124670992