1. 程式人生 > >C#集合類型大揭秘

C#集合類型大揭秘

alt log 不錯 exc try 快速查找 沖突 slim color

集合是.NET FCL(Framework Class Library)的重要組成部分,我們平常擼C#代碼時免不了和集合打交道,FCL提供了豐富易用的集合類型,給我們擼碼提供了極大的便利。正是因為這種與生俱來的便利性,使得我們對集合既熟悉又陌生。很多同學可能一直還是停留在使用的層面上,那麽今天我們一起來深入學習一下C#語言中的各種集合。

首先我們看一下 FCL 給我們提供的集合接口:

技術分享圖片

FCL提供了泛型非泛型兩大類集合類型。因為非泛型集合裝箱和拆箱帶來的性能開銷問題,和泛型集合相比,已經變得越來越雞肋。所以我們也側重於泛型集合的分析,但是兩者差別不大。

IEnumerable和IEnumerator

技術分享圖片

IEnumerable接口是所有集合類型的祖宗接口,其作用相當於Object類型之於其它類型。如果某個類型實現了IEnumerable接口,就意味著它可以被叠代訪問,也就可以稱之為集合類型(可枚舉)。IEnumerable接口定義非常簡單,只有一個GetEnumerator()方法用於獲取IEnumerator類型的叠代器。

技術分享圖片

我們可以將叠代器想象成數據庫的遊標,即序列(集合)中的某個位置,叠代器只能在序列(集合)中向前移動。每調用一次MoveNext(),如果序列(集合)中還有下一個元素,則叠代器移動到下一個元素;Current用於獲取序列(集合)中的當前元素;因為叠代器調用一次代碼只需要獲取一個元素,這意味著我們需要確定訪問到了序列(集合)中的哪個位置。Reset()用於重置這種狀態,但是基本上不會使用Reset()重置狀態。

同一個序列(集合)可能同時存在多個叠代器操作,相當於同時對一個集合進行多個遍歷。這種情況下可能會出現叠代彼此交錯。那麽如何解決呢?

集合類不直接支持 IEnumerator
IEnumerator 接口。而是直接支持 IEnumerable接口,其唯一方法是 GetEnumerator,此方法用於返回支持 IEnumerator 的對象。每次調用GetEnumerator()方法時都需要創建一個新的對象,同時叠代器必須保存自身的狀態,記錄此時已經叠代到哪一個元素。這樣叠代器就像是序列中的遊標。可以有多個遊標,移動其中任何一個都可以枚舉集合,與其他叠代器互不影響。

foreach是怎麽實現的?

for依賴對 Length 屬性和索引運算符 ([]) 的支持。借助 Length 屬性,C# 編譯器可以使用 for 語句叠代數組中的每個元素。for適用於長度固定且始終支持索引運算符的數組,但並不是所有類型集合的元素數量都是已知的。此外,許多集合類(包括 Stack、Queue 和 Dictionary)都不支持按索引檢索元素。因此,需要使用一種更為通用的方法來叠代元素集合。假設可以確定第一個、第二個和最後一個元素,那麽就沒有必要知道元素數量,也沒有必要支持按索引檢索元素。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];
    }
}

IEnumerableIEnumerator雖然實現簡單,只有簡單的幾個成員,但是卻支撐起了C#語言中集合這座高樓大廈。

ICollection和ICollection

從第一張圖中,我們可以得知ICollection繼承於IEnumerable接口,並且擴展了IEnumerable接口。

技術分享圖片

主要擴展的功能有:

  1. 新增了屬性Count,用於記錄集合元素個數
  2. 支持添加元素和移除元素
  3. 支持是否包含某元素
  4. 支持清空集合等等

對於任何實現了ICollection接口的集合,我們都可以通過第1條Count屬性獲取當前集合的元素數,所以這些集合也被稱為計數集合。

IList和IList

技術分享圖片

IList接口直接繼承於ICollection接口和IEnumerable接口,並且擴展了通過索引操作集合的功能。

主要擴展的功能有:

  1. 通過索引獲取集合中某個元素
  2. 通過元素獲取元素在集合中的索引值
  3. 通過索引插入元素到集合指定位置
  4. 移除集合指定索引處的元素

IDictionary和IDictionary

技術分享圖片

IDictionary接口直接繼承於ICollection接口和IEnumerable接口,存儲的元素是鍵值對,擴展了通過操作鍵值對集合的功能。

主要擴展的功能有:

  1. 通過鍵KEY獲取值VALUE
  2. 插入新的鍵值對{KEY:VALUE}
  3. 是否包含KEY
  4. 通過KEY移除鍵值對元素

主要的集合的接口介紹完了,下面我們來看一下具體的集合類型。

關聯性泛型集合類

1.Dictionary

Dictionary的查詢數據所花費的時間是所有集合類裏面最快的,因為其內部使用了散列函數加雙數組來實現,所以其查詢數據操作的時間復雜度可以認為是O(1)。Dictionary的實現是一種典型的犧牲空間換取時間(雙數組)的做法。

技術分享圖片

Dictionary添加新元素的實現:

技術分享圖片

技術分享圖片

Dictionary內部有兩個數組,一個數組名為buckets,用於存放由多個同義詞組成的靜態鏈表頭指針(鏈表的第一個元素在數組中的索引號,當它的值為-1時表示此哈希地址不存在元素);另一個數組為entries,它用於存放哈希表中的實際數據,同時這些數據通過next指針構成多個單鏈表。entries數組中所存放的是Entry結構體,Entry結構體由4個部分組成,如下所示:

技術分享圖片

Dictionary計算key的哈希值使用的是取余法,這種方式可能會產生沖突,所以進行沖突解決。Dictionary解決沖突的方式是鏈接法。

技術分享圖片

我們可以根據源碼來模擬推導一下這個過程:

當添加第一個元素時,此時會分配哈希表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解決沖突的方式是鏈接法,把發生沖突的元素鏈接之前元素的後面,通過next屬性來指定沖突關系,最後更新哈希表buckets數組。此時內部結構如圖所示:

技術分享圖片

我們可以通過Dictionary查找元素的實現來證明我們上面的分析是正確的。

Dictionary查找元素的實現:

技術分享圖片

技術分享圖片

Dictionary之所以能實現快速查找元素,其內部使用哈希表來存儲元素對應的位置,我們可以通過哈希值快速地從哈希表中定位元素所在的位置索引,從而快速獲取到key對應的Value值。物極必反,Dictionary的缺點也很明顯,就是裏面的數據是無序排列的,所以按照一定順序遍歷查找數據效率是非常低的。

2.SortedDictionary

SortedDictionaryDictionary類似,至於區別我們從名稱上就可以看出來,Dictionary是無序的,SortedDictionary則是有序的。key要保證唯一,而且還要有序排列,這讓我們很自然的就想到了搜索二叉樹。SortedDictionary使用一種平衡搜索二叉樹——紅黑樹,作為存儲結構。因為基於二分查找,所以添加、查找、刪除元素的時間復雜度是O(log n)。相對於下面提到的SortedList來說,SortedDictionary在添加和刪除元素時更快一些。如果想要快速查詢的同時又能很好的支持排序的話,並且添加和刪除元素也比較頻繁,可以使用SortedDictionary

SortedDictionary添加新元素的實現:

技術分享圖片

技術分享圖片

3.SortedList

在既需要快速查找又需要順序排列的場景下,Dictionary就無能為力了,因為Dictionary使用了散列函數,並不支持線性排序。我們可以使用SortedList集合類來應對這種場景。

SortedList集合內部是使用數組實現的,添加和刪除元素的時間復雜度是O(n),查找元素利用了二分查找,所以查找元素的時間復雜度是O(log n)。所以SortedList雖然支持了有序排列,但是卻是以犧牲查找效率為代價的。

SortedListSortedDictionary同時支持快速查詢和排序,SortedList 優勢在於使用的內存比 SortedDictionary 少;但是SortedDictionary可對未排序的數據執行更快的插入和移除操作:它的時間復雜度為 O(log n),而 SortedList 為 O(n)。所以SortedList適用於既需要快速查找又需要順序排列但是添加和刪除元素較少的場景。

內部實現結構:

技術分享圖片

根據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,只不過Dictionary存儲的鍵值對對象,而HashSet存儲的是普通對象。其內部實現也和Dictionary基本一致,也是散列函數加雙數組實現的,區別是存儲的Slot結構體不再有key。

內部實現數據結構:

技術分享圖片

m_slots中所存放的是Slot結構體,Slot結構體由3個部分組成,如下所示:

技術分享圖片

添加新元素的具體實現:

Dictionary添加新元素的實現基本一致。

技術分享圖片

4.SortedSet

SortedSetHashSet,就像SortedDictionaryDictionary一樣。SortedSet支持元素按順序排列,內部實現也是紅黑樹,並且SortedSet對於紅黑樹的操作方法和SortedDictionary完全相同。所以不再做過多的分析。

5.Stack

棧是一種後進先出的結構,C#的棧是借助數組實現的,考慮到棧後進先出的特性,使用數組來實現貌似是水到渠成的事。

技術分享圖片

入棧操作:

技術分享圖片

彈棧操作:

技術分享圖片

6.Queue

隊列是一種先進先出的結構,C#的隊列也是借助數組實現的,有了前面的經驗,借助數組實現必然會有數組擴容。C#的隊列實現其實是循環隊列的方式,可以簡單的理解為將隊列的頭尾相接。至於為什麽要這麽做?為了節省存儲空間和減少元素的移動。因為元素出隊列時後面的元素跟著前移是非常消耗性能的,但是不跟著向前移動的話,前面就會一直存在空閑的空間浪費內存。所以使用循環隊列來解決這種問題。

技術分享圖片

入隊操作:

技術分享圖片

技術分享圖片

出隊操作:

技術分享圖片

線程安全的集合類

需要我們註意的是,上面我們所介紹的集合並不是線程安全的,在多線程環境下,可能會出現線程安全問題。在多線程讀的情況下,我們使用普通集合即可。在多線程添加/更新/刪除時,我們可以采用手動鎖定的方式確保線程安全,但是應該註意加鎖的範圍和粒度,加鎖不當可能會導致程序性能低下甚至產生死鎖。

更好的選擇的是使用的C#提供的線程安全集合(命名空間:System.Collections.Concurrent)。線程安全集合使用幾種算法來最小化線程阻塞。

技術分享圖片

  1. ConcurrentQueue: 線程安全版本的Queue
  2. ConcurrentStack:線程安全版本的Stack
  3. ConcurrentBag:線程安全的對象集合
  4. ConcurrentDictionary:線程安全的Dictionary

總結

寫著寫著突然發現跑到數據結構上來了。程序=數據結構+算法。上面提到的集合類型,我們需要在不同的場景進行合適的選擇,其實本質上就是選擇合適的數據結構。

參考:

https://www.cnblogs.com/jesse2013/p/CollectionsInCSharp.html

https://www.c-sharpcorner.com/article/concurrent-collections-in-net-concurrentdictionary-part-one/

http://www.cnblogs.com/jeffwongishandsome/archive/2012/09/09/2677293.html

http://www.cnblogs.com/edisonchou/p/4706253.html



作者:擼碼那些事
來源:http://songwenjie.cnblogs.com/
聲明:本文為博主學習感悟總結,水平有限,如果不當,歡迎指正。如果您認為還不錯,不妨點擊一下下方的推薦按鈕,謝謝支持。轉載與引用請註明出處。
微信公眾號:
技術分享圖片

C#集合類型大揭秘