.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++;
return;
}
}
這裡Insert
函式裡就會走進for
迴圈,不過"c"不是已經有的key
,hashcode
匹配不到所以if就不會進了。
狀態如圖:
從圖上看到,新新增的entry
的index
給到第0個bucket
的value
(黃色線),而bucket
上一次的value
(紅色線)也就是上次新增的元素的index
給到新新增entry
的next
,這樣通過bucket
得到最新的entry
,而不停的通過entry
的next
就可以把同一個bucket
下的entry
都遍歷到。
dict["d"]="D" -> Resize()
再用索引器的方式加入"d
",
public TValue this[TKey key]
{
set
{
Insert(key, value, false);
}
}
也是insert
,不過第三個引數是false
,這樣insert
裡碰到相同的key
會替換掉而不是像Add
那樣拋異常,這個還是不會走到if
裡去,因為key
不重複
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;
}
不過由於容量已經滿了,現在會走到下面這段程式碼:
if (count == entries.Length)
{
Resize();
targetBucket = hashCode % buckets.Length;
}
觸發Resize
,看看Resize
程式碼:
private void Resize()
{
Resize(HashHelpers.ExpandPrime(count), false);
}
先通過HashHelpers.ExpandPrime(count)
取到下個容量大小。
public static int ExpandPrime(int oldSize)
{
int newSize = 2 * oldSize; //新size為兩倍當前大小
if ((uint)newSize > MaxPrimeArrayLength && MaxPrimeArrayLength > oldSize)//這裡MaxPrimeArrayLength是int32.MaxValue,size當然不能超過int32的最大值
{
Debug.Assert(MaxPrimeArrayLength == GetPrime(MaxPrimeArrayLength), "Invalid MaxPrimeArrayLength");
return MaxPrimeArrayLength;
}
return GetPrime(newSize);//這個上面講過,是取比新size大的第一個質數
}
所以resize
的容量不是2倍也不是上面那個質數陣列往後找,而是比2倍大的第一個質數。那現在是3,2倍是6,下一個質數是7,擴容的目標是7。
再詳細看resize
實現:
private void Resize(int newSize, bool forceNewHashCodes)
{
Contract.Assert(newSize >= entries.Length);
int[] newBuckets = new int[newSize];
for (int i = 0; i < newBuckets.Length; i++) newBuckets[i] = -1; //重置buckets
Entry[] newEntries = new Entry[newSize];
Array.Copy(entries, 0, newEntries, 0, count); //建立新entries並把舊的entries複製進去
if (forceNewHashCodes) // 強制更新hashcode,dictionary不會走進去
{
for (int i = 0; i < count; i++)
{
if (newEntries[i].hashCode != -1)
{
newEntries[i].hashCode = (comparer.GetHashCode(newEntries[i].key) & 0x7FFFFFFF);
}
}
}
for (int i = 0; i < count; i++) //因為重置了buckets,所以這裡遍歷entries來重新建立bucket和entry的關係
{
if (newEntries[i].hashCode >= 0) //hashcode做了正數處理,不應該都是大於0的麼,其實不然,remove裡講hashcode為什麼會為負
{
int bucket = newEntries[i].hashCode % newSize;
newEntries[i].next = newBuckets[bucket];
newBuckets[bucket] = i; //還是insert裡的那一套,同一個bucket index, bucket指向最新的entry的index, 而新entry的next就指向老的entry的index,迴圈下去
}
}
buckets = newBuckets;
entries = newEntries;
}
因為大小變了,取餘也就不一樣,所以entry
和bucket
對應的位置也不同了,不過沒影響。
Resize
消耗不低,比List<T>
的要大,不光要copy
元素,還要重建bucket
。
Resize
後繼續上面那一套,看狀態圖:
"d"的hashcode
為11,餘數是4(現在大小是7了哈),與"b"碰撞,所以next就指到"b"的index
,而bucket
則去記新新增的"d"了(典型的喜新厭舊,有沒有)。
dict[“a”]=“AA”
"a"已經新增過了,再次用索引器新增"a"就走了if
裡面
if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key))
{
if (add) //如果用Add方法會拋異常
{
throw new ArgumentException(SR.Format(SR.Argument_AddingDuplicate, key));
}
entries[i].value = value; //替換掉目標entry的值
version++;
return; //這裡直接return了,因為只是替換值,與bucket關係並沒有改變
}
這步就非常之簡單,只是"A"替換成"AA"。
Remove("b")
來看看Remove
程式碼:
public bool Remove(TKey key)
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
if (buckets != null)
{
int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
int bucket = hashCode % buckets.Length; //先算出hashcode
int last = -1; //last初始為-1
for (int i = buckets[bucket]; i >= 0; last = i, i = entries[i].next) //last在迴圈時指向上一個entry的index
{
if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) //先找到相同的key
{
if (last < 0) //小於0說明是第1個,last只有初始為-1
{
buckets[bucket] = entries[i].next; //remove第一個的話就只要把bucket的值指向要remove的entry的下一個就好了,這樣連結串列就繼續存在,只是把頭去掉了。
}
else
{
entries[last].next = entries[i].next; //remove中間或最後的entry就讓上一個的next指向下一個的index,可以想像在連結串列中間去掉一個,是不是得把上下兩邊再連起來
}
entries[i].hashCode = -1; //把hashcode置為-1,上面有說hashcode有可能為負,這裡就為負數了
entries[i].next = freeList; //freeList在這裡用到了, 把刪除的entry的next指向freeList,現在為-1
entries[i].key = default(TKey); //key和value都設為預設值,這裡因為是string所以都是null
entries[i].value = default(TValue);
freeList = i; //freeList就指向這空出來的entry的index
freeCount++; //freeCount加一個,這裡可以知道freeCount是用來記entries裡空出來的個數
version++;
return true;
}
}
}
return false;
}
這裡可以看出Dictionary
並不像List
那樣Remove
,Dictionary
為了效能並沒有在Remove
做重建,而是把位置空出來,這樣節省大量時間。freeList
和bucket
類似(一樣喜新厭舊),總是指向最新空出來的entry
的index
,而entry
的next
又把所有空的entry
連起來了。這樣insert
時就可以先找到這些空填進去。
這裡"d"的next
本來是指向"b"的,Remove(b)
後把"b"的next
給了"d"(下面那條紅線),這樣繼續保持連結串列狀態。freeList
和freeCount
這裡就知道了是用來記住刪除元素的index
和個數。
Add("e", "E")
這裡再新增一個,因為有空了,所以會優先補上空出來的。
if (freeCount > 0) //freeCount大於0,所以進來了
{
index = freeList; //當前index指向最新空出來的
freeList = entries[index].next; //把freeList再指到下一個,保持連結串列
freeCount--; //用掉一個少一個
}
"e"的hashcode
為10,所以也在index
為3的bucket
裡,bucket value
指向剛新增的entry
也就是1,而這個entry
的next
就指向bucket
舊的那個。這樣就把空出來的又補上了。
通過上面分析,對Dictionary
新增和刪除的原理已經清楚了,這樣下面的也會非常容易理解。
var a = dict["a"]
來看看索引器的get
public TValue this[TKey key]
{
get
{
int i = FindEntry(key);
if (i >= 0) return entries[i].value;
throw new KeyNotFoundException();
}
}
是通過FindEntry
來找到entry
進而得到value
private int FindEntry(TKey key)
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
if (buckets != null)
{
int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF; //取hashcode
for (int i = buckets[hashCode % buckets.Length]; i >= 0; i = entries[i].next) //遍歷bucket連結串列
{
if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) return i; //找到hashcode一致的,也就是同樣的key,返回entry索引
}
}
return -1;//沒找到key,後面就拋KeyNotFoundException了
}
var hasA = dict.ContainsKey("a")
看看ContainsKey
程式碼:
public bool ContainsKey(TKey key)
{
return FindEntry(key) >= 0;
}
和上面一樣,通過FindEntry
來找索引,索引不為-1
就是包含。
其他
看看Dictionary
還有哪些值得注意的:
public int Count
{
get { return count - freeCount; }
}
真正的count
是entries
裡個數減去裡面空著的。
public bool ContainsValue(TValue value)
{
if (value == null)
{
for (int i = 0; i < count; i++)
{
if (entries[i].hashCode >= 0 && entries[i].value == null) return true;
}
}
else
{
EqualityComparer<TValue> c = EqualityComparer<TValue>.Default;
for (int i = 0; i < count; i++)
{
if (entries[i].hashCode >= 0 && c.Equals(entries[i].value, value)) return true;
}
}
return false;
}
ContainsValue
和ContainsKey
就不一樣了,它沒有bucket
可以匹配,只能遍歷entries
,所以效能和List
的Contains
一樣,使用時需要注意。
另外還有不少程式碼是為了實現Enumerator
,畢竟Dictionary
支援KeyValuePair
, Key
, Value
三種方式遍歷,其實這三種遍歷都是對Entries
陣列的遍歷,這裡就不多做分析了。
總結
Dictionary
的預設初始容量為3,並在填滿時自動擴容,以比當前值的2倍大的第一個質數(固定質數陣列裡的)作為擴容目標。
Dictionary
也不是執行緒安全,多執行緒環境下需要我們自己加鎖,和List
一樣也是通過version
來確保遍歷時集合不被修改。
Dictionary
的遍歷有三種,KeyValuePair
,Key
, Value
,這三個本質都是遍歷entries
陣列。
Dictionary
取值快速的原理是因為通過buckets
來建立了Key
與entry
之前的聯絡,通過Key
的hashcode
算出bucket
的index
,而bucket
的value
指向entry
的index
,這樣快速得到entry
的value
,當然也有不同的key
指向同一個bucket
,所以bucket
的index
總是指向最新的entry
,而有衝突的entry
又通過next
連線,這樣即使有衝突也只要遍歷很少的entry
就可以取到值,Dictionary
在元素越多時效能優勢越明顯。
當然Dictionary
為取值快也是付出了一點小代價,就是通過空間換取時間,多加了buckets
這個陣列來建立key
與entry
的聯絡,另外還有entry
結構裡的hashcode
和next
,不過相比速度這點代價基本可以忽略了。
下面是上面例子的整個過程圖:(右鍵在新標籤頁開啟)
相關文章
- .net原始碼分析 - ConcurrentDictionary泛型原始碼泛型
- 【java】【泛型】泛型geneticJava泛型
- SOFA 原始碼分析 — 泛化呼叫原始碼
- 通過.net core原始碼看下Dictionary的實現原始碼
- 泛型型別(.NET 指南)泛型型別
- .NET泛型解析(上)泛型
- .NET泛型解析(下)泛型
- NET 2.0中泛型泛型
- Java™ 教程(泛型原始型別)Java泛型型別
- .net原始碼分析 – List原始碼
- .net core 原始碼分析原始碼
- 【科普】.NET6 泛型泛型
- Netty Channel原始碼分析Netty原始碼
- Netty原始碼分析----PoolChunkNetty原始碼
- netty : NioEventLoopGroup 原始碼分析NettyOOP原始碼
- AFNetworking原始碼分析原始碼
- CBNetworking:原始碼分析原始碼
- 從Dictionary原始碼看雜湊表原始碼
- Netty原始碼分析-- FastThreadLocal分析(十)Netty原始碼ASTthread
- Netty 原始碼分析-目錄Netty原始碼
- Netty原始碼分析(四):EventLoopGroupNetty原始碼OOP
- Netty原始碼分析(五):EventLoopNetty原始碼OOP
- Netty原始碼分析--NIO(一)Netty原始碼
- Netty原始碼分析(七) PoolChunkNetty原始碼
- 【Netty】原始碼分析目錄Netty原始碼
- AFNetworking 原始碼分析(一)原始碼
- 對.NET2.0泛型初步理解泛型
- Java泛型知識點:泛型類、泛型介面和泛型方法Java泛型
- 泛型類、泛型方法及泛型應用泛型
- Netty原始碼分析(一):Netty總覽Netty原始碼
- Netty 原始碼分析系列(一)Netty 概述Netty原始碼
- Java中泛型的詳細解析,深入分析泛型的使用方式Java泛型
- Netty原始碼分析--Reactor模型(二)Netty原始碼React模型
- Netty原始碼分析--建立Channel(三)Netty原始碼
- Netty原始碼分析之LengthFieldBasedFrameDecoderNetty原始碼
- NettyPipeline原始碼分析(2)Netty原始碼
- Netty4.x原始碼分析Netty原始碼
- Kubernetes Deployment 原始碼分析(一)原始碼