1. 程式人生 > >.net原始碼分析 – Dictionary泛型

.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型陣列,具體什麼用現在還未知,後面看,暫時可以理解成區,像硬碟我們一般會做分割槽歸類方便查詢。

entriesEntry陣列,看看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, 說明我們Dictionarykeyvalue

就是用這個結構儲存的,另外還有hashcodenext,看起來像連結串列一樣,後面用到時再具體分析其用處。

count:和List <T>一樣,是指包括元素的個數(這裡其實也不是真正的個數,下面會講),並不是容量

version: List <T>篇講過,用來遍歷時禁止修改集合

freeList, freeCount這兩個看起來比較奇怪,比較難想到會有什麼用,在新增和刪除項時會用到它們,後面再講。

comparer: key的比較物件,可以用它來獲取hashcode以及進行比較key是否相同

keys, values這個我們平常也有用到,遍歷keysvalues有用

_syncRootList<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,增加一個就要擴容很沒必要。

GetPrimeif (prime >= min) return prime;這行程式碼知道是要獲取第一個比傳進來的值大的質數,比方傳的是1,那3就是獲取到的初始容量。

接著看初始化部分的程式碼:size現在知道是3,接下來以這個size來初始化bucketsentries,並且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,看下面程式碼是我們用到的stringcomparerhashcode直接取的stringhashcode,其實這裡面的所有型別取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,用keyhashcode來對長度取餘,取到的餘是0到(length-1)之前一個數,最好的情況全部分散開,每個key正好對應一個bucket,也就是entries裡每一項都對應一個bucket,就可以形成下圖取value的過程:
在這裡插入圖片描述
這個取值過程非常快,因為沒有任何遍歷。但實際情況是hashcode取的餘不會正好都不同,總有可能會有一些重複的,那這些重複的是怎麼處理的呢,還是先繼續看Insert的程式碼:

變數狀態如下圖:
在這裡插入圖片描述
從這圖可以看出來是由hashcode得到bucketindex(紫色線),而bucketvalue是指向entryindex(黃色線), entrynext又指向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++;