zoukankan      html  css  js  c++  java
  • .net源码分析 – Dictionary<TKey, TValue>

    接上篇:.net源码分析 – List<T>

    Dictionary<TKey, TValue>源码地址:https://github.com/dotnet/corefx/blob/master/src/System.Collections/src/System/Collections/Generic/Dictionary.cs

    接口

    Dictionary<TKey, TValue>和List<T>的接口形式差不多,不重复说了,可以参考List<T>那篇。

    变量

    看下有哪些成员变量:

     1 private int[] buckets;
     2 private Entry[] entries;
     3 private int count;
     4 private int version;
     5 
     6 private int freeList;
     7 private int freeCount;
     8 private IEqualityComparer<TKey> comparer;
     9 private KeyCollection keys;
    10 private ValueCollection values;
    11 private Object _syncRoot;

    buckets是一个int型数组,具体什么用现在还未知,后面看,暂时可以理解成区,像硬盘我们一般会做分区归类方便查找。

    entries是Entry数组,看看Entry:

    1 private struct Entry
    2 {
    3     public int hashCode;    // Lower 31 bits of hash code, -1 if unused
    4     public int next;        // Index of next entry, -1 if last
    5     public TKey key;           // Key of entry
    6     public TValue value;         // Value of entry
    7 }

    是个结构,里面有key, value, 说明我们Dictionary的key和value就是用这个结构保存的,另外还有hashcode和next,看起来像链表一样,后面用到时再具体分析其用处。

    count:和List <T>一样,是指包括元素的个数(这里其实也不是真正的个数,下面会讲),并不是容量

    version: List <T>篇讲过,用来遍历时禁止修改集合

    freeList, freeCount这两个看起来比较奇怪,比较难想到会有什么用,在添加和删除项时会用到它们,后面再讲。

    comparer: key的比较对象,可以用它来获取hashcode以及进行比较key是否相同

    keys, values这个我们平常也有用到,遍历keys或values有用

    _syncRoot,List<T>篇也讲过,线程安全方面的,Dictionary同样没有用到这个对象,Dictionary也不是线程安全的,在多线程环境下使用需要自己加锁。

    例子

    Dictionary的代码比List相对复杂些,下面不直接分析源码,而是以下面这些常用例子来一步一步展示Dictionary是怎么工作的:

     1 Dictionary<string, string> dict = new Dictionary<string, string>();
     2 
     3 dict.Add("a", "A");
     4 
     5 dict.Add("b", "B");
     6 
     7 dict.Add("c", "C");
     8 
     9 dict["d"] = "D";
    10 
    11 dict["a"] = "AA";
    12 
    13 dict.remove("b");
    14 
    15 dict.Add("e", "E");
    16 
    17 var a = dict["a"];
    18 
    19 var hasA = dict.ContainsKey("a");

    这里对hashcode做些假设,方便分析:

    "a"的hashcode为3

    "b"的hashcode为4

    "c"的hashcode为6

    "d"的hashcode为11

    "e"的hashcode为10

    构造函数

    先看第一句,new 一个Dictionary<string, string>,看源码里的构造函数,有6个

     1 public Dictionary() : this(0, null) { }
     2 
     3 public Dictionary(int capacity) : this(capacity, null) { }
     4 
     5 public Dictionary(IEqualityComparer<TKey> comparer) : this(0, comparer) { }
     6 
     7 public Dictionary(int capacity, IEqualityComparer<TKey> comparer)
     8 {
     9     if (capacity < 0) throw new ArgumentOutOfRangeException(nameof(capacity), capacity, "");
    10     if (capacity > 0) Initialize(capacity);
    11     this.comparer = comparer ?? EqualityComparer<TKey>.Default;
    12 }
    13 
    14 public Dictionary(IDictionary<TKey, TValue> dictionary) : this(dictionary, null) { }
    15 
    16 public Dictionary(IDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey> comparer) :
    17     this(dictionary != null ? dictionary.Count : 0, comparer)
    18 {
    19     if (dictionary == null)
    20     {
    21         throw new ArgumentNullException(nameof(dictionary));
    22     }
    23     if (dictionary.GetType() == typeof(Dictionary<TKey, TValue>))
    24     {
    25         Dictionary<TKey, TValue> d = (Dictionary<TKey, TValue>)dictionary;
    26         int count = d.count;
    27         Entry[] entries = d.entries;
    28         for (int i = 0; i < count; i++)
    29         {
    30             if (entries[i].hashCode >= 0)
    31             {
    32                 Add(entries[i].key, entries[i].value);
    33             }
    34         }
    35         return;
    36     }
    37 
    38     foreach (KeyValuePair<TKey, TValue> pair in dictionary)
    39     {
    40         Add(pair.Key, pair.Value);
    41     }
    42 }

    大部分都是用默认值,真正用到的是public Dictionary(int capacity, IEqualityComparer<TKey> comparer),这个是每个构造函数都要调用的,看看它做了什么:

    if (capacity > 0) Initialize(capacity); 当capacity大于0时,也就是显示指定了capacity时才会调用初始化函数,capacity指容量,List<T>里也有说过,不同的是Dictionary只能在构造函数里指定capacity,而List<T>可以随时指定。接下来看看初始化函数做了什么:

    1 private void Initialize(int capacity)
    2 {
    3     int size = HashHelpers.GetPrime(capacity);
    4     buckets = new int[size];
    5     for (int i = 0; i < buckets.Length; i++) buckets[i] = -1;
    6     entries = new Entry[size];
    7     freeList = -1;
    8 }

    HashHelpers.GetPrime(capacity)根据传进来的capacity获取一个质数,质数大家都知道 2,3,5,7,11,13等等除了自身和1,不能被其他数整除的就是质数,具体看看这个获取质数的函数:

     1 public static readonly int[] primes = {
     2     3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919,
     3     1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591,
     4     17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, 130363, 156437,
     5     187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263,
     6     1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369, 8639249, 10367101,
     7     12440537, 14928671, 17914409, 21497293, 25796759, 30956117, 37147349, 44576837, 53492207, 64190669,
     8     77028803, 92434613, 110921543, 133105859, 159727031, 191672443, 230006941, 276008387, 331210079,
     9     397452101, 476942527, 572331049, 686797261, 824156741, 988988137, 1186785773, 1424142949, 1708971541,
    10     2050765853, MaxPrimeArrayLength };
    11         
    12 public static int GetPrime(int min)
    13 {
    14     if (min < 0)
    15         throw new ArgumentException("");
    16     Contract.EndContractBlock();
    17 
    18     for (int i = 0; i < primes.Length; i++)
    19     {
    20         int prime = primes[i];
    21         if (prime >= min) return prime;
    22     }
    23 
    24     return min;
    25 }

    这里维护了个质数数组,注意,里面并不是完整的质数序列,而是有一些过滤掉了,因为有些挨着太紧,比方说2和3,增加一个就要扩容很没必要。

    GetPrime看if (prime >= min) return prime;这行代码知道是要获取第一个比传进来的值大的质数,比方传的是1,那3就是获取到的初始容量。

    接着看初始化部分的代码:size现在知道是3,接下来以这个size来初始化buckets和entries,并且buckets里的元素都设为-1,freeList同样初始化成-1,这个后面有用。

    初始化完后再调用这行代码 : this.comparer = comparer ?? EqualityComparer<TKey>.Default; 也是初始化comparer,看EqualityComparer<TKey>.Default这个到底用的是什么:

     1 public static EqualityComparer<T> Default
     2 {
     3     get
     4     {
     5         if (_default == null)
     6         {
     7             object comparer;
     8                     
     9             if (typeof(T) == typeof(SByte))
    10                 comparer = new EqualityComparerForSByte();
    11             else if (typeof(T) == typeof(Byte))
    12                 comparer = new EqualityComparerForByte();
    13             else if (typeof(T) == typeof(Int16))
    14                 comparer = new EqualityComparerForInt16();
    15             else if (typeof(T) == typeof(UInt16))
    16                 comparer = new EqualityComparerForUInt16();
    17             else if (typeof(T) == typeof(Int32))
    18                 comparer = new EqualityComparerForInt32();
    19             else if (typeof(T) == typeof(UInt32))
    20                 comparer = new EqualityComparerForUInt32();
    21             else if (typeof(T) == typeof(Int64))
    22                 comparer = new EqualityComparerForInt64();
    23             else if (typeof(T) == typeof(UInt64))
    24                 comparer = new EqualityComparerForUInt64();
    25             else if (typeof(T) == typeof(IntPtr))
    26                 comparer = new EqualityComparerForIntPtr();
    27             else if (typeof(T) == typeof(UIntPtr))
    28                 comparer = new EqualityComparerForUIntPtr();
    29             else if (typeof(T) == typeof(Single))
    30                 comparer = new EqualityComparerForSingle();
    31             else if (typeof(T) == typeof(Double))
    32                 comparer = new EqualityComparerForDouble();
    33             else if (typeof(T) == typeof(Decimal))
    34                 comparer = new EqualityComparerForDecimal();
    35             else if (typeof(T) == typeof(String))
    36                 comparer = new EqualityComparerForString();
    37             else
    38                 comparer = new LastResortEqualityComparer<T>();
    39 
    40             _default = (EqualityComparer<T>)comparer;
    41         }
    42 
    43         return _default;
    44     }
    45 }

    为不同类型创建一个comparer,看下面代码是我们用到的string的comparer:hashcode直接取的string的hashcode,其实这里面的所有类型取hashcode都是一样,equals则有个别不同。

     1 internal sealed class EqualityComparerForString : EqualityComparer<String>
     2 {
     3     public override bool Equals(String x, String y)
     4     {
     5         return x == y;
     6     }
     7 
     8     public override int GetHashCode(String x)
     9     {
    10         if (x == null)
    11             return 0;
    12         return x.GetHashCode();
    13     }
    14 }

    基本构造函数就这些,还有个构造函数可以传一个IDictionary<TKey, TValue>进来,和List<T>一样,也是初始化就加入这些集合,首先判断是否是Dictionary,是的话直接遍历它的entries,加到当前的entries里,如果不是则用枚举器遍历。

    为什么不直接用枚举器呢,因为枚举器也是要消耗一些资源的,而且没有直接遍历数组来得快。

    这个构造函数添加时用到了Add方法,和例子里Add一样,正好是接下来要讲的。

    Add("a", "A")

    下图就是初始变量的状态:

    Add方法直接调用Insert方法,第三个参数为true

    1 public void Add(TKey key, TValue value)
    2 {
    3     Insert(key, value, true);
    4 }

    再看Insert方法,这个方法是核心方法,有点长,跟着注释一点一点看。

     1 private void Insert(TKey key, TValue value, bool add)
     2 {
     3     if (key == null)
     4     {
     5         throw new ArgumentNullException(nameof(key));
     6     }
     7     //首先如果buckets为空则初始化,第一次调用会走到这里,以0为capacity初始化,根据上面的分析,获得的初始容量是3,也就是说3是Dictionary<Tkey, TValue>的默认容量。
     8     if (buckets == null) Initialize(0); 
     9 
    10     //取hashcode后还与0x7FFFFFFF做了个与操作,0x7FFFFFFF这就是int32.MaxValue的16进制,换成二进制是‭01111111111111111111111111111111‬,第1位是符号位,也就是说comparer.GetHashCode(key) 为正数的情况下与0x7FFFFFFF做 & 操作结果还是它本身,如果取到的hashcode是负数,负数的二进制是取反再补码,所以结果得到的是0x7FFFFFFF-(-hashcode)+1,结果是正数。其实简单来说,它的目的就是高性能的取正数。‬‬
    11     int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
    12 
    13     //用得到的新hashcode与buckets的大小取余,得到一个目标bucket索引
    14     int targetBucket = hashCode % buckets.Length;
    15 
    16     //做个遍历,初始值为buckets[targetBucket],现在"a"的hashcode为3,这样targetBucket现在是0,buckets[0]是-1,i是要>=0的,循环走不下去,跳出
    17     for (int i = buckets[targetBucket]; i >= 0; i = entries[i].next)
    18     {
    19         if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key))
    20         {
    21             if (add)
    22             {
    23                 throw new ArgumentException(SR.Format(SR.Argument_AddingDuplicate, key));
    24             }
    25             entries[i].value = value;
    26             version++;
    27             return;
    28         }
    29     }
    30 
    31     int index;
    32     //freeCount也是-1,走到else里面
    33     if (freeCount > 0)
    34     {
    35         index = freeList;
    36         freeList = entries[index].next;
    37         freeCount--;
    38     }
    39     else
    40     {
    41         //count是元素的个数0, entries经过初始化后目前length是3,所以不用resize
    42         if (count == entries.Length)
    43         {
    44             Resize();
    45             targetBucket = hashCode % buckets.Length;
    46         }
    47         //index = count说明index指向entries数组里当前要写值的索引,目前是0
    48         index = count;
    49 
    50         //元素个数增加一个
    51         count++;
    52     }
    53 
    54     //把key的hashcode存到entries[0]里的hashcode,免得要用时重复计算hashcode
    55     entries[index].hashCode = hashCode;
    56     //entries[0]的next指向buckets[0]也就是-1
    57     entries[index].next = buckets[targetBucket];
    58     //设置key和value
    59     entries[index].key = key;
    60     entries[index].value = value;
    61     //再让buckets[0] = 0
    62     buckets[targetBucket] = index;
    63     //这个不多说,不知道的可以看List<T>篇
    64     version++;
    65 }

    看到这里可以先猜一下用bucket的目的,dictionary是为了根据key快速得到value,用key的hashcode来对长度取余,取到的余是0到(length-1)之前一个数,最好的情况全部分散开,每个key正好对应一个bucket,也就是entries里每一项都对应一个bucket,就可以形成下图取value的过程:

    这个取值过程非常快,因为没有任何遍历。但实际情况是hashcode取的余不会正好都不同,总有可能会有一些重复的,那这些重复的是怎么处理的呢,还是先继续看Insert的代码:

    变量状态如下图:

    从这图可以看出来是由hashcode得到bucket的index(紫色线),而bucket的value是指向entry的index(黄色线), entry的next又指向bucket上一次的value(红色线),是不是有链表的感觉。

    Add("b", "B")

    由于"b"的hashcode为4,取余得1,并没有和现有的重复,所以流程和上面一样(左边的线不用看,属于上面流程)

    Add("c", "C")

    "c"的hashcode是6,取余得0,得到也是在第0个bucket,这样就产生碰撞了,

     1 for (int i = buckets[targetBucket]; i >= 0; i = entries[i].next)
     2 {
     3     if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key))
     4     {
     5         if (add)
     6         {
     7             throw new ArgumentException(SR.Format(SR.Argument_AddingDuplicate, key));
     8         }
     9         entries[i].value = value;
    10         version++;
    11         return;
    12     }
    13 }

    这里Insert函数里就会走进for循环,不过"c"不是已经有的key,hashcode匹配不到所以if就不会进了。

    状态如图:

    从图上看到,新添加的entry的index给到第0个bucket的value (黄色线),而bucket上一次的value(红色线)也就是上次添加的元素的index给到新添加entry的next,这样通过bucket得到最新的entry,而不停的通过entry的next就可以把同一个bucket下的entry都遍历到。

    dict["d"]="D" -> Resize()

    再用索引器的方式加入"d",

    1 public TValue this[TKey key]
    2 {
    3     set
    4     {
    5         Insert(key, value, false);
    6     }
    7 }

    也是insert,不过第三个参数是false,这样insert里碰到相同的key会替换掉而不是像Add那样抛异常,这个还是不会走到if里去,因为key不重复

     1 if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key))
     2 {
     3     if (add)
     4     {
     5         throw new ArgumentException(SR.Format(SR.Argument_AddingDuplicate, key));
     6     }
     7     entries[i].value = value;
     8     version++;
     9     return;
    10 }

    不过由于容量已经满了,现在会走到下面这段代码:

    1 if (count == entries.Length)
    2 {
    3     Resize();
    4     targetBucket = hashCode % buckets.Length;
    5 }

    触发Resize,看看Resize代码:

    1 private void Resize()
    2 {
    3     Resize(HashHelpers.ExpandPrime(count), false);
    4 }

    先通过HashHelpers.ExpandPrime(count)取到下个容量大小。

     1 public static int ExpandPrime(int oldSize)
     2 {
     3     int newSize = 2 * oldSize; //新size为两倍当前大小
     4     if ((uint)newSize > MaxPrimeArrayLength && MaxPrimeArrayLength > oldSize)//这里MaxPrimeArrayLength是int32.MaxValue,size当然不能超过int32的最大值
     5     {
     6         Debug.Assert(MaxPrimeArrayLength == GetPrime(MaxPrimeArrayLength), "Invalid MaxPrimeArrayLength");
     7 
     8         return MaxPrimeArrayLength;
     9     }
    10 
    11     return GetPrime(newSize);//这个上面讲过,是取比新size大的第一个质数
    12 }

    所以resize的容量不是2倍也不是上面那个质数数组往后找,而是比2倍大的第一个质数。那现在是3,2倍是6,下一个质数是7,扩容的目标是7。

    再详细看resize实现:

     1 private void Resize(int newSize, bool forceNewHashCodes)
     2 {
     3     Contract.Assert(newSize >= entries.Length);
     4     int[] newBuckets = new int[newSize];
     5     for (int i = 0; i < newBuckets.Length; i++) newBuckets[i] = -1;  //重置buckets
     6 
     7     Entry[] newEntries = new Entry[newSize];
     8     Array.Copy(entries, 0, newEntries, 0, count);  //建立新entries并把旧的entries复制进去
     9 
    10     if (forceNewHashCodes) // 强制更新hashcode,dictionary不会走进去
    11     {
    12         for (int i = 0; i < count; i++)
    13         {
    14             if (newEntries[i].hashCode != -1)
    15             {
    16                 newEntries[i].hashCode = (comparer.GetHashCode(newEntries[i].key) & 0x7FFFFFFF);
    17             }
    18         }
    19     }
    20 
    21     for (int i = 0; i < count; i++) //因为重置了buckets,所以这里遍历entries来重新建立bucket和entry的关系
    22     {
    23         if (newEntries[i].hashCode >= 0)  //hashcode做了正数处理,不应该都是大于0的么,其实不然,remove里讲hashcode为什么会为负
    24         {
    25             int bucket = newEntries[i].hashCode % newSize;
    26             newEntries[i].next = newBuckets[bucket];
    27             newBuckets[bucket] = i;  //还是insert里的那一套,同一个bucket index, bucket指向最新的entry的index, 而新entry的next就指向老的entry的index,循环下去
    28         }
    29     }
    30 
    31     buckets = newBuckets;
    32     entries = newEntries;
    33 }

    因为大小变了,取余也就不一样,所以entry和bucket对应的位置也不同了,不过没影响。

    Resize消耗不低,比List<T>的要大,不光要copy元素,还要重建bucket。

    Resize后继续上面那一套,看状态图:

    "d"的hashcode为11,余数是4(现在大小是7了哈),与"b"碰撞,所以next就指到"b"的index,而bucket则去记新添加的"d"了(典型的喜新厌旧,有没有)。

    dict["a"]="AA"

    "a"已经添加过了,再次用索引器添加"a"就走了if里面

     1 if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key))
     2 {
     3     if (add) //如果用Add方法会抛异常
     4     {
     5         throw new ArgumentException(SR.Format(SR.Argument_AddingDuplicate, key));
     6     }
     7     entries[i].value = value; //替换掉目标entry的值
     8     version++;
     9     return;  //这里直接return了,因为只是替换值,与bucket关系并没有改变
    10 }

    这步就非常之简单,只是"A"替换成"AA"。

    Remove("b")

    来看看Remove代码:

     1 public bool Remove(TKey key)
     2 {
     3     if (key == null)
     4     {
     5         throw new ArgumentNullException(nameof(key));
     6     }
     7 
     8     if (buckets != null)
     9     {
    10         int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
    11         int bucket = hashCode % buckets.Length;  //先算出hashcode
    12         int last = -1;  //last初始为-1
    13         for (int i = buckets[bucket]; i >= 0; last = i, i = entries[i].next)  //last在循环时指向上一个entry的index
    14         {
    15             if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) //先找到相同的key
    16             {
    17                 if (last < 0)  //小于0说明是第1个,last只有初始为-1
    18                 {
    19                     buckets[bucket] = entries[i].next; //remove第一个的话就只要把bucket的值指向要remove的entry的下一个就好了,这样链表就继续存在,只是把头去掉了。
    20                 }
    21                 else
    22                 {
    23                     entries[last].next = entries[i].next;  //remove中间或最后的entry就让上一个的next指向下一个的index,可以想像在链表中间去掉一个,是不是得把上下两边再连起来
    24                 }
    25                 entries[i].hashCode = -1; //把hashcode置为-1,上面有说hashcode有可能为负,这里就为负数了
    26                 entries[i].next = freeList;  //freeList在这里用到了, 把删除的entry的next指向freeList,现在为-1
    27                 entries[i].key = default(TKey); //key和value都设为默认值,这里因为是string所以都是null
    28                 entries[i].value = default(TValue);
    29                 freeList = i;  //freeList就指向这空出来的entry的index
    30                 freeCount++;  //freeCount加一个,这里可以知道freeCount是用来记entries里空出来的个数
    31                 version++;
    32                 return true;
    33             }
    34         }
    35     }
    36     return false;
    37 }

    这里可以看出Dictionary并不像List那样Remove,Dictionary为了性能并没有在Remove做重建,而是把位置空出来,这样节省大量时间。freeList和bucket类似(一样喜新厌旧),总是指向最新空出来的entry的index,而entry的next又把所有空的entry连起来了。这样insert时就可以先找到这些空填进去。

    这里"d"的next本来是指向"b"的,Remove(b)后把"b"的next给了"d"(下面那条红线),这样继续保持链表状态。freeList和freeCount这里就知道了是用来记住删除元素的index和个数。

    Add("e", "E")

    这里再添加一个,因为有空了,所以会优先补上空出来的。

    1 if (freeCount > 0)  //freeCount大于0,所以进来了
    2 {
    3     index = freeList;  //当前index指向最新空出来的
    4     freeList = entries[index].next;  //把freeList再指到下一个,保持链表
    5     freeCount--;  //用掉一个少一个
    6 }

    "e"的hashcode为10,所以也在index为3的bucket里,bucket value指向刚添加的entry也就是1,而这个entry的next就指向bucket旧的那个。这样就把空出来的又补上了。

    通过上面分析,对Dictionary添加和删除的原理已经清楚了,这样下面的也会非常容易理解。

    var a = dict["a"]

    来看看索引器的get

    1 public TValue this[TKey key]
    2 {
    3     get
    4     {
    5         int i = FindEntry(key);
    6         if (i >= 0) return entries[i].value;
    7         throw new KeyNotFoundException();
    8     }
    9 }

    是通过FindEntry来找到entry进而得到value

     1 private int FindEntry(TKey key)
     2 {
     3     if (key == null)
     4     {
     5         throw new ArgumentNullException(nameof(key));
     6     }
     7 
     8     if (buckets != null)
     9     {
    10         int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;  //取hashcode
    11         for (int i = buckets[hashCode % buckets.Length]; i >= 0; i = entries[i].next)  //遍历bucket链表
    12         {
    13             if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) return i; //找到hashcode一致的,也就是同样的key,返回entry索引
    14         }
    15     }
    16     return -1;//没找到key,后面就抛KeyNotFoundException了
    17 }

    var hasA = dict.ContainsKey("a")

    看看ContainsKey代码:

    1 public bool ContainsKey(TKey key)
    2 {
    3     return FindEntry(key) >= 0;
    4 }

    和上面一样,通过FindEntry来找索引,索引不为-1就是包含。

    其他

    看看Dictionary还有哪些值得注意的:

    1 public int Count
    2 {
    3     get { return count - freeCount; }
    4 }

    真正的count是entries里个数减去里面空着的。

     1 public bool ContainsValue(TValue value)
     2 {
     3     if (value == null)
     4     {
     5         for (int i = 0; i < count; i++)
     6         {
     7             if (entries[i].hashCode >= 0 && entries[i].value == null) return true;
     8         }
     9     }
    10     else
    11     {
    12         EqualityComparer<TValue> c = EqualityComparer<TValue>.Default;
    13         for (int i = 0; i < count; i++)
    14         {
    15             if (entries[i].hashCode >= 0 && c.Equals(entries[i].value, value)) return true;
    16         }
    17     }
    18     return false;
    19 }

    ContainsValue和ContainsKey就不一样了,它没有bucket可以匹配,只能遍历entries,所以性能和List的Contains一样,使用时需要注意。

    另外还有不少代码是为了实现Enumerator,毕竟Dictionary支持KeyValuePair, Key, Value三种方式遍历,其实这三种遍历都是对Entries数组的遍历,这里就不多做分析了。

    总结

    Dictionary的默认初始容量为3,并在填满时自动扩容,以比当前值的2倍大的第一个质数(固定质数数组里的)作为扩容目标。

    Dictionary也不是线程安全,多线程环境下需要我们自己加锁,和List一样也是通过version来确保遍历时集合不被修改。

    Dictionary的遍历有三种,KeyValuePair,Key, Value,这三个本质都是遍历entries数组。

    Dictionary取值快速的原理是因为通过buckets来建立了Key与entry之前的联系,通过Key的hashcode算出bucket的index,而bucket的value指向entry的index,这样快速得到entry的value,当然也有不同的key指向同一个bucket,所以bucket的index总是指向最新的entry,而有冲突的entry又通过next连接,这样即使有冲突也只要遍历很少的entry就可以取到值,Dictionary在元素越多时性能优势越明显。

    当然Dictionary为取值快也是付出了一点小代价,就是通过空间换取时间,多加了buckets这个数组来建立key与entry的联系,另外还有entry结构里的hashcode和next,不过相比速度这点代价基本可以忽略了。

    下面是上面例子的整个过程图:(右键在新标签页打开)

  • 相关阅读:
    继承与多态,Instanceof关键字
    面向对象,单例模式
    方法
    数组
    流程控制
    基础语法
    连接linux四大远程工具
    MYSQL-索引的设计
    银行一类(Ⅰ类)、二类(Ⅱ类)、三类(Ⅲ类)账户区别是什么?
    真正有效的学习
  • 原文地址:https://www.cnblogs.com/brookshi/p/5402820.html
Copyright © 2011-2022 走看看