標籤:
一、前言
在之前有一次面試中,被問到你瞭解Dictionary的內部實現機制嗎?當時只是簡單的了問答了:Dictionary的內部結構是雜湊表,從而可以快速進行尋找。但是對於更深一步瞭解就不清楚了。所以面試回來之後,就打算好好研究下Dictionary的源碼。所以也就有了這篇文章。
二、Dictionary源碼剖析
大家都知道,現在微軟已經開源了.NET Framework的源碼了,線上源碼查看地址為:http://referencesource.microsoft.com/。通過尋找可以找到.NET Framework類的源碼。下面我們就一起來看下Dictionary源碼。
2.1 添加元素
首先我們來查看下Dictionary.Add方法的實現。為了讓大家更好地實現,下面抽取了Dictionary源碼核心部分來進行分析,詳細的分析代碼如下所示:
// buckets是雜湊表,用來存放Key的Hash值 // entries用來存放元素列表 // count是元素數量 private void Insert(TKey key, TValue value, bool add) { if (key == null) { throw new ArgumentNullException(key.ToString()); } // 首先分配buckets和entries的空間 if (buckets == null) Initialize(0); int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF; // 計算key值對應的雜湊值(HashCode) int targetBucket = hashCode % buckets.Length; // 對雜湊值求餘,獲得需要對雜湊表進行賦值的位置#if FEATURE_RANDOMIZED_STRING_HASHING int collisionCount = 0;#endif // 處理衝突的處理邏輯 for (int i = buckets[targetBucket]; i >= 0; i = entries[i].next) { if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) { if (add) { throw new ArgumentNullException(); } entries[i].value = value; version++; return; }#if FEATURE_RANDOMIZED_STRING_HASHING collisionCount++;#endif } int index; // index記錄了元素在元素列表中的位置 if (freeCount > 0) { index = freeList; freeList = entries[index].next; freeCount--; } else { // 如果雜湊表存放雜湊值已滿,則重新從primers數組中取出值來作為雜湊表新的大小 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; version++;#if FEATURE_RANDOMIZED_STRING_HASHING if(collisionCount > HashHelpers.HashCollisionThreshold && HashHelpers.IsWellKnownEqualityComparer(comparer)) { comparer = (IEqualityComparer<TKey>) HashHelpers.GetRandomizedEqualityComparer(comparer); Resize(entries.Length, true); }#endif }
下面以一個實際的添加例子來具體分析下上面的添加元素代碼,從而更好地理解Add方法的實現原理。
Dictionary<int, string> myDictionary = new Dictionary<int, string>(); myDictionary.Add(1, "Item 1"); myDictionary.Add(2, "Item 2"); myDictionary.Add(3, "Item 3");
當添加第一個元素時,此時會分配雜湊表buckets數組和entries數組的空間和初始大小為3,分配完成之後,會計算添加元素key值的雜湊值,雜湊值的計算由具體的雜湊演算法來實現的,假設1的雜湊值為9的話,此時targetBucket = 9%buckets.Length(3)的值為0,index的值為0,則第一個元素存放在entries列表中的第一個位置,最後對雜湊表進行賦值,此時賦值的位置為第0個位置,其值為index的值,所以為0,插入第一個元素後Dictionary的內部結構如下所示:
後面添加元素的過程依次類推。其原理就是,buckets記錄了元素的在元素列表的儲存位置,也就相當於一個映射列表。在尋找的時候,就可以通過key值的雜湊值來與buckets數組長度求餘來獲得元素在元素列表中的索引,這樣就可以快速定位元素的位置,從而獲得元素的key對應的Value值。如上面的例子中,如果想找到key值為1對應的Value值時,此時計算1的雜湊值為9,然後對buckets數組長度求餘,此時獲得的值正是0,這樣就可以直接從entries[0].Value的方式來擷取對應的Value的值,這也就是Dictionary能完成快速尋找的實現原理。後面會通過Dictionary內部的尋找源碼來證實上面分析的過程。
2.2 解決衝突
在添加元素過程中,有一個很重要的問題,如果產生衝突怎麼辦?即如果我後面需要插入的一個元素(假設這個值為11吧)的key值的雜湊值也為6,此時targetBucket的值也是為0,但元素列表中0的位置已經存放了元素了,這樣就出現了衝突,那Dictionary是怎樣處理這個衝突的呢?處理衝突的方法有很多種,Dictionary處理的方式是連結法。Dictionary會把發生衝突的元素連結之前元素的後面,通過next屬性來指定衝突關係。此時Dictionary內部結構如所示:
三、Dictionary如何?快速尋找呢?
針對於Dictionary實現快速尋找的原因,在上面我們已經做了一個推斷了,下面通過Dictionary內部的代碼實現來驗證下,具體的尋找代碼如下所示:
public TValue this[TKey key] { get { int i = FindEntry(key); // 通過元素所在存在的位置直接擷取其對應的Value if (i >= 0) return entries[i].value; throw new KeyNotFoundException(); return default(TValue); } set { Insert(key, value, false); } } private int FindEntry(TKey key) { if (key == null) { throw new ArgumentNullException(); } if (buckets != null) { // 獲得Key值對應的雜湊值 int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF; // 尋找元素在元素列表中的位置,如果沒有衝突的情況下,此時尋找速度為O(1),存在衝突的情況下為O(N),N為存在衝突的次數 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; }
通過代碼可以看出,我們之前的分析是完成正確的。從中可以明白:Dictionary之所以能實現快速尋找元素,其內部使用雜湊表來儲存元素對應的位置,然後我們可以通過雜湊值快速地從雜湊表中定位元素所在的位置索引,從而快速擷取到key對應的Value值。
四、總結
可以說,Dictionary的實現原理也是一種空間換時間的思路,多使用一個buckets的儲存空間來儲存元素的位置,從而來提升尋找速度。
接下來,我們新開一個領域驅動設計系列,還請大家多多拍磚。
本文所有源碼下載:DictonaryInDepth.zip
[C#進階系列]專題二:你知道Dictionary尋找速度為什麼快嗎?