集合是.NET FCL(Framework Class Library)的重要組成部分,咱們日常擼C#代碼時免不了和集合打交道,FCL提供了豐富易用的集合類型,給咱們擼碼提供了極大的便利。正是由於這種與生俱來的便利性,使得咱們對集合既熟悉又陌生。不少同窗可能一直仍是停留在使用的層面上,那麼今天咱們一塊兒來深刻學習一下C#語言中的各類集合。算法
數據結構就是相互之間存在一種或多種特定關係的數據元素的集合。 程序界有一點很經典的話,程序設計=數據結構+算法。用源代碼來體現,數據結構,就是編程數據庫
在上圖中能夠看到,集合整體上分爲線性集合和非線性集合。線性集合按照存儲方式又分爲直接存儲和順序存儲。編程
直接存儲,是指該類型的集合數據元素能夠直接經過下標(即index)來訪問,在C#中直接存儲的數據結構有三類:Array(包含數組和List<T>)、string、struct。c#
直接存儲結構的優勢是:向數據結構中添加元素是很高效的,直接放在數據末尾的第一個空位上就能夠了。數組
直接存儲結構的缺點是:向集合插入元素將會變得低效,它須要給插入的元素騰出位置並順序移動後面的元素。安全
順序存儲結構,即線性表。線性表可動態的擴大和縮小,它在一片連續的區域中存儲數據元素。線性表不能按照索引進行查找,它是經過對地址的引用來搜索元素的,爲了找到某個元素,它必須遍歷全部元素,直到找到對應的元素爲止。因此,線性表的優勢是插入和刪除數據效率高,缺點是查找的效率相對來講低一點。數據結構
線性表有能夠分爲隊列、棧以及索引羣集,在C#中分別表現爲:Queue<T>、Stack<T>,索引羣集又進一步泛化爲字典類型Dictionary<TKey,TValue>和雙向鏈表LinkedList<T>。多線程
非線性集合本身在實際應用中比較少,並且感受也比較複雜,因此在此先不作討論學習。下面咱們就來一一的學習一下平常使用比較頻繁的集合吧。函數
首先咱們看一下 FCL 給咱們提供的集合接口:性能
FCL提供了泛型和非泛型兩大類集合類型。由於非泛型集合裝箱和拆箱帶來的性能開銷問題,和泛型集合相比,已經變得愈來愈雞肋。因此咱們也側重於泛型集合的分析,可是二者差異不大。
(一)、IEnumerable和IEnumerator
IEnumerable接口是全部集合類型的祖宗接口,其做用至關於Object類型之於其它類型。若是某個類型實現了IEnumerable接口,就意味着它能夠被迭代訪問,也就能夠稱之爲集合類型(可枚舉)。IEnumerable接口定義很是簡單,只有一個GetEnumerator()方法用於獲取IEnumerator類型的迭代器。
咱們能夠將迭代器想象成數據庫的遊標,即序列(集合)中的某個位置,迭代器只能在序列(集合)中向前移動。每調用一次MoveNext(),若是序列(集合)中還有下一個元素,則迭代器移動到下一個元素;Current用於獲取序列(集合)中的當前元素;由於迭代器調用一次代碼只須要獲取一個元素,這意味着咱們須要肯定訪問到了序列(集合)中的哪一個位置。Reset()用於重置這種狀態,可是基本上不會使用Reset()重置狀態。
同一個序列(集合)可能同時存在多個迭代器操做,至關於同時對一個集合進行多個遍歷。這種狀況下可能會出現迭代彼此交錯。那麼如何解決呢?
集合類不直接支持 IEnumerator 和IEnumerator 接口。而是直接支持 IEnumerable接口,其惟一方法是 GetEnumerator,此方法用於返回支持 IEnumerator 的對象。每次調用GetEnumerator()方法時都須要建立一個新的對象,同時迭代器必須保存自身的狀態,記錄此時已經迭代到哪個元素。這樣迭代器就像是序列中的遊標。能夠有多個遊標,移動其中任何一個均可以枚舉集合,與其餘迭代器互不影響。
for依賴對 Length 屬性和索引運算符 ([]) 的支持。藉助 Length 屬性,C# 編譯器可使用 for 語句迭代數組中的每一個元素。for適用於長度固定且始終支持索引運算符的數組,但並非全部類型集合的元素數量都是已知的。此外,許多集合類(包括 Stack、Queue 和 Dictionary<TKey ,TValue>)都不支持按索引檢索元素。所以,須要使用一種更爲通用的方法來迭代元素集合。假設能夠肯定第一個、第二個和最後一個元素,那麼就沒有必要知道元素數量,也沒有必要支持按索引檢索元素。foreach在這種背景下應運而生。實際上,foreach內部使用迭代器的MoveNext和Current完成元素的遍歷。
List<int> list = new List<int>(); List<int>.Enumerator enumerator = list.GetEnumerator(); try { int number; while (enumerator.MoveNext()) { number = enumerator.Current; Console.WriteLine(number); } } finally { enumerator.Dispose(); }
咱們能夠本身實現IEnumerable接口和IEnumerator接口實現自定義集合。
實現自定義可枚舉類型:
public class MySet : IEnumerable { internal object[] values; public MySet(object[] values) { this.values = values; } public IEnumerator GetEnumerator() { return new MySetIterator(this); } }
手寫實現自定義迭代器:
public class MySetIterator : IEnumerator { MySet set; /// <summary> /// 保存迭代到的位置 /// </summary> int position; internal MySetIterator(MySet set) { this.set = set; position = -1; } public object Current { get { if(position==-1||position==set.values.Length) { throw new InvalidOperationException(); } int index = position; return set.values[index]; } } public bool MoveNext() { if(position!=set.values.Length) { position++; } return position < set.values.Length; } public void Reset() { position = -1; } }
測試程序:
object[] values = { "a", "b", "c", "d", "e" }; MySet mySet = new MySet(values); foreach (var item in mySet) { Console.WriteLine(item); }
這個例子也證實了foreach內部使用迭代器的MoveNext和Current完成遍歷。
上面的例子中手寫實現迭代器是十分麻煩的,在c#1.0中這是惟一的方式。在c#2.0中,咱們可使用yield語法糖簡化迭代器。
public IEnumerator GetEnumerator() { for (int i = 0; i < values.Length; i++) { yield return values[i]; } }
IEnumerable和IEnumerator雖然實現簡單,只有簡單的幾個成員,可是卻支撐起了C#語言中集合這座高樓大廈。
(二)、ICollection和ICollection
從第一張圖中,咱們能夠得知ICollection繼承於IEnumerable接口,而且擴展了IEnumerable接口。
主要擴展的功能有:
新增了屬性Count,用於記錄集合元素個數
支持添加元素和移除元素
支持是否包含某元素
支持清空集合等等
對於任何實現了ICollection接口的集合,咱們均可以經過第1條Count屬性獲取當前集合的元素數,因此這些集合也被稱爲計數集合。
一、IList 和IList
IList接口直接繼承於ICollection接口和IEnumerable接口,而且擴展了經過索引操做集合的功能。
主要擴展的功能有:
二、IDictionary<TKey, TValue>和IDictionary
IDictionary接口直接繼承於ICollection接口和IEnumerable接口,存儲的元素是鍵值對,擴展了經過鍵操做鍵值對集合的功能。
主要擴展的功能有:
主要的集合的接口介紹完了,下面咱們來看一下具體的集合類型。
(三)、關聯性泛型集合類
1.Dictionary<TKey,TValue>
Dictionary<TKey,TValue>的查詢數據所花費的時間是全部集合類裏面最快的,由於其內部使用了散列函數加雙數組來實現,因此其查詢數據操做的時間複雜度能夠認爲是O(1)。Dictionary<TKey,TValue>的實現是一種典型的犧牲空間換取時間(雙數組)的作法。
Dictionary<TKey,TValue>添加新元素的實現:
Dictionary<TKey,TValue>內部有兩個數組,一個數組名爲buckets,用於存放由多個同義詞組成的靜態鏈表頭指針(鏈表的第一個元素在數組中的索引號,當它的值爲-1時表示此哈希地址不存在元素);另外一個數組爲entries,它用於存放哈希表中的實際數據,同時這些數據經過next指針構成多個單鏈表。entries數組中所存放的是Entry結構體,Entry結構體由4個部分組成,以下所示:
Dictionary<TKey,TValue>計算key的哈希值使用的是取餘法,這種方式可能會產生衝突,因此須要進行衝突解決。Dictionary<TKey,TValue>解決衝突的方式是連接法。
咱們能夠根據源碼來模擬推導一下這個過程:
當添加第一個元素時,此時會分配哈希表buckets數組和entries數組的空間和初始大小,默認爲3。對key=1進行哈希求值,假設第一個元素的哈希值=9,而後targetBucket = 9%buckets.Length(3)的值爲0,因此第一個元素應該放在entries數組的第一位。最後對哈希表buckets數組賦值,數組索引爲0,值爲0。此時內部結構如圖所示:
而後插入第二個元素,對key=2進行哈希求值,假設第二個元素的哈希值=3,而後targetBucket = 3%buckets.Length (默認是3)的值爲0,因此第二個元素應該放在entries數組的第一位。可是entries數組的第一位已經存在元素了,這就發生了衝突。Dictionary<TKey,TValue>解決衝突的方式是連接法,把發生衝突的元素連接以前元素的後面,經過next屬性來指定衝突關係,最後更新哈希表buckets數組。此時內部結構如圖所示:
咱們能夠經過Dictionary<TKey,TValue>查找元素的實現來證實咱們上面的分析是正確的。
Dictionary<TKey,TValue>查找元素的實現:
Dictionary<TKey,TValue>之因此能實現快速查找元素,其內部使用哈希表來存儲元素對應的位置,咱們能夠經過哈希值快速地從哈希表中定位元素所在的位置索引,從而快速獲取到key對應的Value值。物極必反,Dictionary<TKey,TValue>的缺點也很明顯,就是裏面的數據是無序排列的,因此按照必定順序遍歷查找數據效率是很是低的。
2.SortedDictionary<TKey,TValue>
SortedDictionary<TKey,TValue>和Dictionary<TKey,TValue>相似,至於區別咱們從名稱上就能夠看出來,Dictionary<TKey,TValue>是無序的,SortedDictionary<TKey,TValue>則是有序的。key要保證惟一,並且還要有序排列,這讓咱們很天然的就想到了搜索二叉樹。SortedDictionary<TKey,TValue>使用一種平衡搜索二叉樹——紅黑樹,做爲存儲結構。由於基於二分查找,因此添加、查找、刪除元素的時間複雜度是O(log n)。相對於下面提到的SortedList<TKey,TValue>來講,SortedDictionary<TKey,TValue>在添加和刪除元素時更快一些。若是想要快速查詢的同時又能很好的支持排序的話,而且添加和刪除元素也比較頻繁,可使用SortedDictionary<TKey,TValue>。
SortedDictionary<TKey,TValue>添加新元素的實現:
3.SortedList<TKey,TValue>
在既須要快速查找又須要順序排列的場景下,Dictionary<TKey,TValue>就無能爲力了,由於Dictionary<TKey,TValue>使用了散列函數,並不支持線性排序。咱們可使用SortedList<TKey,TValue>集合類來應對這種場景。
SortedList<TKey,TValue>集合內部是使用數組實現的,添加和刪除元素的時間複雜度是O(n),查找元素利用了二分查找,因此查找元素的時間複雜度是O(log n)。因此SortedList<TKey,TValue>雖然支持了有序排列,可是倒是以犧牲查找效率爲代價的。
SortedList<TKey,TValue>和SortedDictionary<TKey,TValue>同時支持快速查詢和排序,SortedList<TKey,TValue> 優點在於使用的內存比 SortedDictionary<TKey,TValue> 少;可是SortedDictionary<TKey,TValue>可對未排序的數據執行更快的插入和移除操做:它的時間複雜度爲 O(log n),而 SortedList<TKey,TValue> 爲 O(n)。因此SortedList<TKey,TValue>適用於既須要快速查找又須要順序排列可是添加和刪除元素較少的場景。
內部實現結構:
根據Key獲取Value的實現:
IndexOfKey實現:
添加新元素:
添加操做:
1.List
泛型的List 類提供了不限制長度的集合類型,List內部實現使用數據結構是數組。咱們都知道數組是長度固定的,那麼List不限制長度一定須要維護這個數組。實際上List維護了必定長度的數組(默認爲4),當插入元素的個數超過4或初始長度時,會去從新建立一個新的數組,這個新數組的長度是初始長度的2倍,而後將原來的數組賦值到新的數組中。
咱們能夠經過ILSpy看一下List源碼證實咱們上面所說的:
List內部重要變量:
新增元素操做:
新增元素確認數組容量:
真正的數組擴容操做:
數組擴容的場景涉及到對象的建立和賦值,是比較消耗性能的。因此若是能指定一個合適的初始長度,能避免頻繁的對象建立和賦值。再者,由於內部的數據結構是數組,插入和刪除操做須要移動元素位置,因此不適合頻繁的進行插入和刪除操做;可是能夠經過數組下標查找元素。因此List適合讀多寫少的場景。
2.LinkedList
上面咱們提到List適合讀多寫少的場景,那麼一定有一個List適合寫多讀少的場景,就是這貨了——LinkedList。至於爲何適合寫多讀少,熟悉數據結構的同窗應該已經猜到了。由於LinkedList的內部實現使用的是鏈表結構,並且仍是雙向鏈表。直接看源碼:
由於內部實現結構是鏈表,因此能夠在某一個節點前或節點後插入新的元素。
鏈表節點定義:
咱們以在某個節點前插入新元素爲例:
具體的插入操做,注意操做步驟不能顛倒:
3.HashSet
HashSet是一個無序的可以保持惟一性的集合。咱們能夠將HashSet看做是簡化的Dictionary<TKey,TValue>,只不過Dictionary<TKey,TValue>存儲的鍵值對對象,而HashSet存儲的是普通對象。其內部實現也和Dictionary<TKey,TValue>基本一致,也是散列函數加雙數組實現的,區別是存儲的Slot結構體再也不有key。
內部實現數據結構:
m_slots中所存放的是Slot結構體,Slot結構體由3個部分組成,以下所示:
添加新元素的具體實現:
和Dictionary<TKey,TValue>添加新元素的實現基本一致。
4.SortedSet
SortedSet和HashSet,就像SortedDictionary<TKey,TValue>和Dictionary<TKey,TValue>同樣。SortedSet支持元素按順序排列,內部實現也是紅黑樹,而且SortedSet對於紅黑樹的操做方法和SortedDictionary<TKey,TValue>徹底相同。因此再也不作過多的分析。
5.Stack
棧是一種後進先出的結構,C#的棧是藉助數組實現的,考慮到棧後進先出的特性,使用數組來實現貌似是水到渠成的事。
入棧操做:
彈棧操做:
6.Queue
隊列是一種先進先出的結構,C#的隊列也是藉助數組實現的,有了前面的經驗,藉助數組實現必然會有數組擴容。C#的隊列實現實際上是循環隊列的方式,能夠簡單的理解爲將隊列的頭尾相接。至於爲何要這麼作?爲了節省存儲空間和減小元素的移動。由於元素出隊列時後面的元素跟着前移是很是消耗性能的,可是不跟着向前移動的話,前面就會一直存在空閒的空間浪費內存。因此使用循環隊列來解決這種問題。
入隊操做:
出隊操做:
須要咱們注意的是,上面咱們所介紹的集合並非線程安全的,在多線程環境下,可能會出現線程安全問題。在多線程讀的狀況下,咱們使用普通集合便可。在多線程添加/更新/刪除時,咱們能夠採用手動鎖定的方式確保線程安全,可是應該注意加鎖的範圍和粒度,加鎖不當可能會致使程序性能低下甚至產生死鎖。
更好的選擇的是使用的C#提供的線程安全集合(命名空間:System.Collections.Concurrent)。線程安全集合使用幾種算法來最小化線程阻塞。