浅谈C#中Dictionary的实现。
引言
Dictionary在C#中算是比較常用的數據結構了,它的讀取速度很快,效率很高。以前沒有怎么看它的源碼實現,前幾天看了看它的源碼實現,還是有點意思的,下面我將逐步說下它的實現原理。
數據結構
它是通過Hash Bucket和鏈表形成的數據結構,將一份數據分為多個鏈表,且每個鏈表都對應它的Bucket。可以看以下的圖:
看不明白不要急,我們先看源碼Dictionary類里面定義的字段都有什么。
private struct Entry {
public int hashCode; // 每個K/V對應的Hash值
public int next; // 指向下一個K/V的index,-1代表最后一個
public TKey key; // key的值
public TValue value; // Value的值
}
private int[] buckets; //定義桶的數組
private Entry[] entries; //定義元素的數組
private int count; //元素總數量
private int version; //版本
private int freeList; //被移除后,空閑元素的下標
private int freeCount; //空閑元素的數量
有了上面字段的定義,接下來我分分析它是怎么添加元素,尋找元素和刪除元素的。
添加元素
首先我們先把主要的源碼貼出來,從源碼上分析主要實現。
private void Insert(TKey key, TValue value, bool add) {
if( key == null ) {
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
}
if (buckets == null) Initialize(0); //初始化上面的那些字段
int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
int targetBucket = hashCode % buckets.Length; //獲取對應的目標Bucket
//尋找是否有相同key的元素
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);
}
entries[i].value = value;
version++;
return;
}
}
int index;
if (freeCount > 0) { //判斷現在是否有空閑的元素,優先使用空閑的元素
index = freeList;
freeList = entries[index].next;
freeCount--;
}
else {
if (count == entries.Length) //判斷是否存儲的項和Entries的長度,相等的話,就重新擴容。
{
Resize();// 擴容Buctet和Entries的大小
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++;
}
源碼詳細講解。
初次添加元素時,如果構造函數中不傳入大小,默認會自動取一個最小的質數(即:3)來作為桶的大小和元素集合的大小,并且初始換里面的字段變量。
在尋找元素插入的位置的時候,首先通過元素的hashcode % bucket的長度優先得到要插入的目標的Bucket是哪個,然后將元素的hashCode和next賦值。
這里next賦值的話詳細說一下。首先,我們先分析buckets,它里面的每個bucket保存的是對應鏈表最后一個元素的下標,可以通過最后一行代碼得知,每次給元素賦值之后,當前元素的下標,會賦值給對應的bucket。
而每個新插入元素,只需要當前bucket里面的值賦值給它當前的next指向的index就可以了。
圖示
查找元素
我們還是先簡單看一下主要實現部分的源碼。
private int FindEntry(TKey key) {
if( key == null) {
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
}
if (buckets != null) {
int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF; //首先獲取key的hashCode
//尋址的第一個元素就是對應目標桶里面記錄的index,然后通過對應元素的next指向下一個元素,當next為-1時,就是代表已經到最后一個元素了。
for (int i = buckets[hashCode % buckets.Length]; i >= 0; i = entries[i].next) {
//判斷元素的hashcode和key是否都相等。
if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) return i;
}
}
return -1;
}
源碼詳細講解。
其實這里代碼已經寫的很清楚了,它是先找到目標桶,因為目標桶里面記錄的是它對應鏈表的最后一個元素的下標,然后順著元素的next找,直到找到這個元素為止。
可以仔細想想,這樣的話,每次查找就可以過濾一大批的數據,所以查的速度就更快了,但是當數據量大的時候,也是會有效率問題。
圖示
移除元素
還是先簡單看下源代碼主要實現的部分。
public bool Remove(TKey key) {
if(key == null) {
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
}
if (buckets != null) {
int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
int bucket = hashCode % buckets.Length;
int last = -1; //這個變量主要是記錄上一個元素的下標。
//和上面一樣,先查找要刪除的元素。
for (int i = buckets[bucket]; i >= 0; last = i, i = entries[i].next) {
if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) {
if (last < 0) { //代表第一個元素就是要找的元素
buckets[bucket] = entries[i].next; //把buctet指向的下標,指向下一個元素
}
else {
entries[last].next = entries[i].next; //將上一個元素的下標,指向下一個元素的下標,去掉被刪除的元素。
}
entries[i].hashCode = -1;
entries[i].next = freeList; //當前元素指向上一個空閑元素的下標
entries[i].key = default(TKey);
entries[i].value = default(TValue);
freeList = i; //記錄最后一個被移除元素的下標
freeCount++; //每次移除,空閑的元素+1
version++;
return true;
}
}
}
return false;
}
源碼詳細講解。
查找對應的元素和上面的邏輯其實是一樣的,它這里定義了一個變量,用來記錄上一個元素的下標。
當找到對應的元素時,把上個元素的next指向當前被移除元素的next,即把當前被移除的元素跳過去。
然后將被移除元素的字段初始化,需要注意的是這個next的值,它用的是freelist,記錄最后一個被移除元素的index,每移除一個元素,被移除的元素數量就+1,即freecount。
被移除的元素也會形成一個鏈表,它的next首部元素next指向-1,后邊被移除的元素next指向上一個被移除元素的index。
回過頭再去看添加的時候,它會判斷,freeCount的數量是否是大于0的,如果大于0的話,優先使用被移除元素的位置并填充它們,它的index就是freeList,然后再把當前元素的next賦值給
freelist(即下次再插入元素的時候,就是上一個被移除元素的下標)。
最后在下面給當前元素賦值的時候,它的next又指向當前bucket里面的值,即作為對應鏈表的尾部。
圖示
關于擴容
先簡單看下主要實現部分的源碼。
private void Resize(int newSize, bool forceNewHashCodes) {
Contract.Assert(newSize >= entries.Length); // 這個newSize是獲取大于count的最小質數
int[] newBuckets = new int[newSize];
for (int i = 0; i < newBuckets.Length; i++) newBuckets[i] = -1; //初始化每個bucket的值
Entry[] newEntries = new Entry[newSize];
Array.Copy(entries, 0, newEntries, 0, count); //將原來的entries的值copy到新的entries里面
for (int i = 0; i < count; i++) {
if (newEntries[i].hashCode >= 0) { //判斷hashcode代表是有效的entry
int bucket = newEntries[i].hashCode % newSize;
newEntries[i].next = newBuckets[bucket];
newBuckets[bucket] = i; //上面的操作就是重新找新的桶,然后重新給entey的next賦值。
}
}
buckets = newBuckets;
entries = newEntries;
}
源碼詳細講解
這里的newSize會在里面的元素數達到entries的長度是擴容,它是在一個helper類里面進行取值,它是拿大于它的最小質數。
這個新的size就是buckets的長度和entries的長度,先把原來的entries的值copy到新的entries里面。
然后循環新的的entries,重新定義新的桶的值并且給原來的數據,重新形成新的鏈表。
總結
關于dictionary的解析也是我一個簡單的理解吧,主要還是看源碼能了解的更多,希望能夠幫助到需要的人。
微軟源碼的地址可以看這個: https://referencesource.microsoft.com/
總結
以上是生活随笔為你收集整理的浅谈C#中Dictionary的实现。的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: oracle离线文档查dbms_Orac
- 下一篇: 【计算机网络】五层体系结构