咱們知道Dictionary的最大特色就是能夠經過任意類型的key尋找值。並且是經過索引,速度極快。
該特色主要意義:數組能經過索引快速尋址,其餘的集合基本都是以此爲基礎進行擴展而已。 但其索引值只能是int,某些情境下就顯出Dictionary的便利性了。
那麼問題就來了--C#是怎麼作的呢,能使其作到泛型索引。html
咱們關注圈中的內容,這是Dictionary的本質 --- 兩個數組,。這是典型的用空間換取時間的作法。
先來講下兩數組分別表明什麼。
1- buckets,int[] ,水桶!不過我以爲用倉庫更爲形象。eg: buckets = new int[3]; 表示三個倉庫,i = buckets [0] ,if i = -1 表示該倉庫爲空,不然表示第一個倉庫存儲着東西。這個東西表示數組entries的索引。
2- entries , Entry<TKey,TValue>[] ,Entry是個結構,key,value就是咱們的鍵值真實值,hashCode是key的哈希值,next能夠理解爲指針,這裏先不具體展開。數組
[StructLayout(LayoutKind.Sequential)] private struct Entry { public int hashCode; public int next; public TKey key; public TValue value; }
先說一下索引,如何用人話來解釋呢?這麼說吧,自己操做系統只支持地址尋址,如數組聲明時會先存一個header,同時獲取一個base地址指向這個header,其後的元素都是經過*(base+index)來進行尋址。
基於這個共識,Dictionary用泛型key索引的實現就得千方百計把key轉換到上面數組索引上去。性能優化
也就是說要在記錄的存儲位置和它的關鍵字之間創建一個肯定的對應關係 f,使每一個關鍵字和結構中一個唯一的存儲位置相對應。
於是在查找時,只要根據這個對應關係 f 找到給定值 K 的函數值 f(K)。若結構中存在關鍵字和 K 相等的記錄。在此,咱們稱這個對應關係 f 爲哈希 (Hash) 函數,按這個思想創建的表爲哈希表。函數
回到Dictionary,這個f(K)就存在於key跟buckets之間:性能
dic[key]加值的實現:entries數組加1,獲取i-->key-->獲取hashCode-->f(hashCode)-->肯定key對應buckets中的某個倉庫(buckets對應的索引)-->設置倉庫裏的東西(entries的索引 = i)
dic[key]取值的實現:key-->獲取hashCode-->f(hashCode)-->肯定key對應buckets中的某個倉庫(buckets對應的索引)--> 獲取倉庫裏的東西(entries的索引i,上面有說到)-->真實的值entries[i]優化
上面的流程中只有一個(f(K)獲取倉庫索引)讓咱們很難受,由於不認識,那如今問題變成了這個f(K)如何實現了。
實現:this
` int index = hashCode % buckets.Length;操作系統
這叫作除留餘數法,哈希函數的其中一種實現。若是你本身寫一個MyDictionary,能夠用其餘的哈希函數。指針
舉個例子,假設兩數組初始大小爲3, this.comparer.GetHashCode(4) & 0x7fffffff = 4:code
Dictionary<int, string> dic = new Dictionary<int, string>(); dic.Add(4, "value");
i=0,key=4--> hashCode=4.GetHashCode()=4--> f(hashCode)=4 % 3 = 1-->第1號倉庫-->東西 i = 0.
此時兩數組狀態爲:
取值按照以前說的順序進行,彷彿已經完美。但這裏還有個問題,不一樣的key生成的hashCode通過f(K)生成的值不是惟一的。即一個倉庫可能會放不少東西。
C#是這麼解決的,每次往倉庫放東西的時候,先判斷有沒有東西(buckets[index] 是否爲 -1),若是有,則進行修改。
如再:
dic.Add(7, "value"); dic.Add(10, "value");
f(entries[1]. hashCode)=7 % 3 = 1也在第一號倉庫,則修改buckets[1] = 1。
同時修改entries[1].next = 0;//上一個倉庫的東西
f(entries[2].hashCode)=10 % 3 = 1也在第一號倉庫,則再修改buckets[1] = 2。
同時修改entries[1].next = 1;//上一個倉庫的東西
這樣至關於1號倉庫存了一個單向鏈表,entries:2-1-0。
成功解決。
這裏有人若是看過這些集合源碼的話知道數組通常會有一個默認大小(固然咱們初始化集合的時候也能夠手動傳入capacity),總之,Length不可能無限大。
那麼當集合滿的時候,咱們需對集合進行擴容,C#通常直接Length*2。那麼buckets.Length就是可變的,上面的f(K)結果也就不是恆定的。
C#對此的解決放在了擴容這一步:
能夠看到擴容實質就是新開闢一個更大空間的數組,講道理是耗資源的。因此咱們在初始化集合的時候,每次都給定一個合適的Capacity,講道理是一個老油條該乾的事兒。
上面說的這就是所謂「用空間換取時間的作法」,兩個數組存了一個集合,而集合中咱們最關心的value彷彿是個主角,一堆配角做陪。
索引器取值:
具體實現:
1,2,3,4,5就是本文的重點。基本都講到了,其中4 ,5 -- (this.entries[i].hashCode == num) && this.comparer.Equals(this.entries[i].key, key):肯定惟一key value對的條件,hashCode相等,key也得相等。
說明hashCode也有相等的狀況,其實這裏 (this.entries[i].hashCode == num)這個條件能夠省略,由於若是key Equal則hashCode 確定相等。固然&&符號會先計算第一個條件,比較hashCode快得多,先過濾掉一大部分元素,最後再用Equals比較肯定。
也就是
hash code 是整數,相等判斷的性能高。
hash code 相等才作較慢的鍵相等判斷。
這是一種性能優化。
Thanks All.
歡迎討論~
感謝閱讀~
我的公衆號: