分析C# Dictionary的实现原理

对于C#中的Dictionary类相信大家都不陌生,这是一个Collection(集合)类型,可以通过Key/Value(键值对的形式来存放数据;该类最大的优点就是它查找元素的时间复杂度接近O(1)。那么什么样的设计能使得Dictionary类实现O(1)的时间复杂度呢

2.7、Dictionary - 再谈Add操作

在我们之前的Add操作步骤中,提到了这样一段话,这里提到会有一种其它的情况,那就是有元素被删除的情况。

避开一种其它情况不谈,接下来它会将hashCode、key、value等信息存入entries[count]中,因为count位置是空闲的;继续count++指向下一个空闲位置。上图中第一个位置,index=0就是空闲的,所以就存放在entries[0]的位置。

因为count是通过自增的方式来指向entries[]下一个空闲的entry,如果有元素被删除了,那么在count之前的位置就会出现一个空闲的entry;如果不处理,会有很多空间被浪费。

这就是为什么Remove操作会记录freeList、freeCount,就是为了将删除的空间利用起来。实际上Add操作会优先使用freeList的空闲entry位置,摘录代码如下。


private void Insert(TKey key, TValue value, bool add){
    
    if( key == null ) {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
    }

    if (buckets == null) Initialize(0);
    // 通过key获取hashCode
    int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
    // 计算出目标bucket下标
    int targetBucket = hashCode % buckets.Length;
	// 碰撞次数
    int collisionCount = 0;
    for (int i = buckets[targetBucket]; i >= 0; i = entries[i].next) {
        if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) {
            // 如果是增加操作,遍历到了相同的元素,那么抛出异常
            if (add) {      
				ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_AddingDuplicate);
            }
            // 如果不是增加操作,那可能是索引赋值操作 dictionary["foo"] = "foo"
            // 那么赋值后版本++,退出
            entries[i].value = value;
            version++;
            return;
        }
        // 每遍历一个元素,都是一次碰撞
        collisionCount++;
    }
    int index;
    // 如果有被删除的元素,那么将元素放到被删除元素的空闲位置
    if (freeCount > 0) {
        index = freeList;
        freeList = entries[index].next;
        freeCount--;
    }
    else {
        // 如果当前entries已满,那么触发扩容
        if (count == entries.Length)
        {
            Resize();
            targetBucket = hashCode % buckets.Length;
        }
        index = count;
        count++;
    }

    // 给entry赋值
    entries[index].hashCode = hashCode;
    entries[index].next = buckets[targetBucket];
    entries[index].key = key;
    entries[index].value = value;
    buckets[targetBucket] = index;
    // 版本号++
    version++;

    // 如果碰撞次数大于设置的最大碰撞次数,那么触发Hash碰撞扩容
    if(collisionCount > HashHelpers.HashCollisionThreshold && HashHelpers.IsWellKnownEqualityComparer(comparer)) 
    {
        comparer = (IEqualityComparer<TKey>) HashHelpers.GetRandomizedEqualityComparer(comparer);
        Resize(entries.Length, true);
    }
}

上面就是完整的Add代码,还是很简单的对不对?

2.8、Collection版本控制

在上文中一直提到了version这个变量,在每一次新增、修改和删除操作时,都会使version++;那么这个version存在的意义是什么呢?

首先我们来看一段代码,这段代码中首先实例化了一个Dictionary实例,然后通过foreach遍历该实例,在foreach代码块中使用dic.Remove(kv.Key)删除元素。

这样的异常,迭代过程中不允许集合出现变化。如果在Java中遍历直接删除元素,会出现诡异的问题,所以.Net中就使用了version来实现版本控制。

那么如何在迭代过程中实现版本控制的呢?我们看一看源码就很清楚的知道。

在迭代器初始化时,就会记录dictionary.version版本号,之后每一次迭代过程都会检查版本号是否一致,如果不一致将抛出异常。

这样就避免了在迭代过程中修改了集合,造成很多诡异的问题。

以上就是分析C# Dictionary的实现原理的详细内容,更多关于C# Dictionary的资料请关注得得之家其它相关文章!

本文标题为:分析C# Dictionary的实现原理