.net原始碼分析 – Dictionary泛型
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>
那篇。
變數
看下有哪些成員變數:
private int[] buckets;
private Entry[] entries;
private int count;
private int version;
private int freeList;
private int freeCount;
private IEqualityComparer<TKey> comparer;
private KeyCollection keys;
private ValueCollection values;
private Object _syncRoot;
buckets
是一個int
型陣列,具體什麼用現在還未知,後面看,暫時可以理解成區,像硬碟我們一般會做分割槽歸類方便查詢。
entries
是Entry
陣列,看看Entry
private struct Entry
{
public int hashCode; // Lower 31 bits of hash code, -1 if unused
public int next; // Index of next entry, -1 if last
public TKey key; // Key of entry
public TValue value; // Value of entry
}
是個結構,裡面有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
是怎麼工作的:
Dictionary<string, string> dict = new Dictionary<string, string>();
dict.Add("a", "A");
dict.Add("b", "B");
dict.Add("c", "C");
dict["d"] = "D";
dict["a"] = "AA";
dict.remove("b");
dict.Add("e", "E");
var a = dict["a"];
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個
public Dictionary() : this(0, null) { }
public Dictionary(int capacity) : this(capacity, null) { }
public Dictionary(IEqualityComparer<TKey> comparer) : this(0, comparer) { }
public Dictionary(int capacity, IEqualityComparer<TKey> comparer)
{
if (capacity < 0) throw new ArgumentOutOfRangeException(nameof(capacity), capacity, "");
if (capacity > 0) Initialize(capacity);
this.comparer = comparer ?? EqualityComparer<TKey>.Default;
}
public Dictionary(IDictionary<TKey, TValue> dictionary) : this(dictionary, null) { }
public Dictionary(IDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey> comparer) :
this(dictionary != null ? dictionary.Count : 0, comparer)
{
if (dictionary == null)
{
throw new ArgumentNullException(nameof(dictionary));
}
if (dictionary.GetType() == typeof(Dictionary<TKey, TValue>))
{
Dictionary<TKey, TValue> d = (Dictionary<TKey, TValue>)dictionary;
int count = d.count;
Entry[] entries = d.entries;
for (int i = 0; i < count; i++)
{
if (entries[i].hashCode >= 0)
{
Add(entries[i].key, entries[i].value);
}
}
return;
}
foreach (KeyValuePair<TKey, TValue> pair in dictionary)
{
Add(pair.Key, pair.Value);
}
}
大部分都是用預設值,真正用到的是public Dictionary(int capacity, IEqualityComparer<TKey> comparer)
,這個是每個建構函式都要呼叫的,看看它做了什麼:
if (capacity > 0) Initialize(capacity);
當capacity
大於0時,也就是顯示指定了capacity
時才會呼叫初始化函式,capacity
指容量,List<T>
裡也有說過,不同的是Dictionary
只能在建構函式裡指定capacity
,而List<T>
可以隨時指定。接下來看看初始化函式做了什麼:
private void Initialize(int capacity)
{
int size = HashHelpers.GetPrime(capacity);
buckets = new int[size];
for (int i = 0; i < buckets.Length; i++) buckets[i] = -1;
entries = new Entry[size];
freeList = -1;
}
HashHelpers.GetPrime(capacity)
根據傳進來的capacity
獲取一個質數,質數大家都知道 2,3,5,7,11,13等等除了自身和1,不能被其他數整除的就是質數,具體看看這個獲取質數的函式:
public static readonly int[] primes = {
3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919,
1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591,
17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, 130363, 156437,
187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263,
1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369, 8639249, 10367101,
12440537, 14928671, 17914409, 21497293, 25796759, 30956117, 37147349, 44576837, 53492207, 64190669,
77028803, 92434613, 110921543, 133105859, 159727031, 191672443, 230006941, 276008387, 331210079,
397452101, 476942527, 572331049, 686797261, 824156741, 988988137, 1186785773, 1424142949, 1708971541,
2050765853, MaxPrimeArrayLength };
public static int GetPrime(int min)
{
if (min < 0)
throw new ArgumentException("");
Contract.EndContractBlock();
for (int i = 0; i < primes.Length; i++)
{
int prime = primes[i];
if (prime >= min) return prime;
}
return min;
}
這裡維護了個質數陣列,注意,裡面並不是完整的質數序列,而是有一些過濾掉了,因為有些挨著太緊,比方說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
這個到底用的是什麼:
public static EqualityComparer<T> Default
{
get
{
if (_default == null)
{
object comparer;
if (typeof(T) == typeof(SByte))
comparer = new EqualityComparerForSByte();
else if (typeof(T) == typeof(Byte))
comparer = new EqualityComparerForByte();
else if (typeof(T) == typeof(Int16))
comparer = new EqualityComparerForInt16();
else if (typeof(T) == typeof(UInt16))
comparer = new EqualityComparerForUInt16();
else if (typeof(T) == typeof(Int32))
comparer = new EqualityComparerForInt32();
else if (typeof(T) == typeof(UInt32))
comparer = new EqualityComparerForUInt32();
else if (typeof(T) == typeof(Int64))
comparer = new EqualityComparerForInt64();
else if (typeof(T) == typeof(UInt64))
comparer = new EqualityComparerForUInt64();
else if (typeof(T) == typeof(IntPtr))
comparer = new EqualityComparerForIntPtr();
else if (typeof(T) == typeof(UIntPtr))
comparer = new EqualityComparerForUIntPtr();
else if (typeof(T) == typeof(Single))
comparer = new EqualityComparerForSingle();
else if (typeof(T) == typeof(Double))
comparer = new EqualityComparerForDouble();
else if (typeof(T) == typeof(Decimal))
comparer = new EqualityComparerForDecimal();
else if (typeof(T) == typeof(String))
comparer = new EqualityComparerForString();
else
comparer = new LastResortEqualityComparer<T>();
_default = (EqualityComparer<T>)comparer;
}
return _default;
}
}
為不同型別建立一個comparer
,看下面程式碼是我們用到的string
的comparer
:hashcode
直接取的string
的hashcode
,其實這裡面的所有型別取hashcode
都是一樣,equals
則有個別不同。
internal sealed class EqualityComparerForString : EqualityComparer<String>
{
public override bool Equals(String x, String y)
{
return x == y;
}
public override int GetHashCode(String x)
{
if (x == null)
return 0;
return x.GetHashCode();
}
}
基本建構函式就這些,還有個建構函式可以傳一個IDictionary<TKey, TValue>
進來,和List<T>
一樣,也是初始化就加入這些集合,首先判斷是否是Dictionary
,是的話直接遍歷它的entries
,加到當前的entries
裡,如果不是則用列舉器遍歷。
為什麼不直接用列舉器呢,因為列舉器也是要消耗一些資源的,而且沒有直接遍歷陣列來得快。
這個建構函式新增時用到了Add
方法,和例子裡Add
一樣,正好是接下來要講的。
Add("a", "A")
下圖就是初始變數的狀態:
Add
方法直接呼叫Insert
方法,第三個引數為true
public void Add(TKey key, TValue value)
{
Insert(key, value, true);
}
再看Insert
方法,這個方法是核心方法,有點長,跟著註釋一點一點看。
private void Insert(TKey key, TValue value, bool add)
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
//首先如果buckets為空則初始化,第一次呼叫會走到這裡,以0為capacity初始化,根據上面的分析,獲得的初始容量是3,也就是說3是Dictionary<Tkey, TValue>的預設容量。
if (buckets == null) Initialize(0);
//取hashcode後還與0x7FFFFFFF做了個與操作,0x7FFFFFFF這就是int32.MaxValue的16進位制,換成二進位制是01111111111111111111111111111111,第1位是符號位,也就是說comparer.GetHashCode(key) 為正數的情況下與0x7FFFFFFF做 & 操作結果還是它本身,如果取到的hashcode是負數,負數的二進位制是取反再補碼,所以結果得到的是0x7FFFFFFF-(-hashcode)+1,結果是正數。其實簡單來說,它的目的就是高效能的取正數。
int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
//用得到的新hashcode與buckets的大小取餘,得到一個目標bucket索引
int targetBucket = hashCode % buckets.Length;
//做個遍歷,初始值為buckets[targetBucket],現在"a"的hashcode為3,這樣targetBucket現在是0,buckets[0]是-1,i是要>=0的,迴圈走不下去,跳出
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 ArgumentException(SR.Format(SR.Argument_AddingDuplicate, key));
}
entries[i].value = value;
version++;
return;
}
}
int index;
//freeCount也是-1,走到else裡面
if (freeCount > 0)
{
index = freeList;
freeList = entries[index].next;
freeCount--;
}
else
{
//count是元素的個數0, entries經過初始化後目前length是3,所以不用resize
if (count == entries.Length)
{
Resize();
targetBucket = hashCode % buckets.Length;
}
//index = count說明index指向entries數組裡當前要寫值的索引,目前是0
index = count;
//元素個數增加一個
count++;
}
//把key的hashcode存到entries[0]裡的hashcode,免得要用時重複計算hashcode
entries[index].hashCode = hashCode;
//entries[0]的next指向buckets[0]也就是-1
entries[index].next = buckets[targetBucket];
//設定key和value
entries[index].key = key;
entries[index].value = value;
//再讓buckets[0] = 0
buckets[targetBucket] = index;
//這個不多說,不知道的可以看List<T>篇
version++;
}
看到這裡可以先猜一下用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
,這樣就產生碰撞了,
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 ArgumentException(SR.Format(SR.Argument_AddingDuplicate, key));
}
entries[i].value = value;
version++;