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,不過相比速度這點代價基本可以忽略了。
下面是上面例子的整個過程圖:(右鍵在新標簽頁打開)