C#集合型別大盤點

騰飛(Jesse)發表於2016-06-18

  C#集體型別( Collections in C#)

  集合是.NET FCL(Framework Class Library)中很重要的一部分,也是我們開發當中最常用到的功能之一,幾乎是無處不在。俗話說知其然,知其所以然,平常看到IEnumerable,IEnumerator,ICollection是不是知道他們之間各自的區別?除了List和Dictionary以外,你還用過哪些其它的集合類?廢話少說,今天我們就來看一些這些定義集合類的介面以及他們的實現。

 集合介面

  先來看一下,FCL為我們提供了哪些介面:

  

  IEnumerable 和IEnumberator

public interface IEnumerator
{
 
    bool MoveNext();
    object Current {  get; }
    void Reset();
}

  IEnumerator定義了我們遍歷集合的基本方法,以便我們可以實現單向向前的訪問集合中的每一個元素。而IEnumerable只有一個方法GetEnumerator即得到遍歷器。

public interface IEnumerable
{
    IEnumerator GetEnumerator();
}

  注意:我們經常用的foreach即是一種語法糖,實際上還是呼叫Enumerator裡面的Current和MoveNext實現的遍歷功能。

List<string> list = new List<string>() 
{ 
    "Jesse",
    "Chloe",
    "Lei",
    "Jim",
    "XiaoJun"
};
 
// Iterate the list by using foreach
foreach (var buddy in list)
{
    Console.WriteLine(buddy);
}
 
// Iterate the list by using enumerator
List<string>.Enumerator enumerator = list.GetEnumerator();
while (enumerator.MoveNext())
{
    Console.WriteLine(enumerator.Current);
}

  上面的程式碼中用到的foreach和enumerator到IL中最後都會被翻譯成enumerator的MoveNext和Current。

  IEnumerable是一個很有用的介面,實現它的好處包括:

  1. 支援foreach語句
  2. 作為一個標準的集合類與其它類庫進行互動
  3. 滿足更復雜的集合介面的需求
  4. 支援集合初始化器

  當然實現的方法也有很多,如下:

  1. 如果我們集合是通過封裝其它集合類而來的,那麼我們可以直接返回這個集合的enumerator
  2. 通過yield return 來返回
  3. 實現我們自己的IEnumerator來實現

  這裡給大家演示一下如何通過yield來實現返回enumerator

public class BuddyList : IEnumerable
{
    private string[] data= new string[]
    { 
        "Jesse",
        "Chloe",
        "Lei",
        "Jim",
        "XiaoJun"
    };
 
    public IEnumerator GetEnumerator()
    {
        foreach (var str in data)
        {
            yield return str;
        }
    }
}
 
var myBuddies= new BuddyList();
foreach (var str in myBuddies)
{
    Console.WriteLine(str);
}

  ICollection<T>和ICollection

  從最上面第一張圖我們可以知道,ICollection是直接繼承自IEnumerable。而實際上也是如此,我們可以說ICollection比IEnumerable多支援一些功能,不僅僅只提供基本的遍歷功能,還包括:

  1. 統計集合和元素個數
  2. 獲取元素的下標
  3. 判斷是否存在
  4. 新增元素到未尾
  5. 移除元素等等。。。

   ICollection 與ICollection<T> 略有不同,ICollection不提供編輯集合的功能,即Add和Remove。包括檢查元素是否存在Contains也不支援。

  IList<T>和IList

  IList則是直接繼承自ICollection和IEnumerable。所以它包括兩者的功能,並且支援根據下標訪問和新增元素。IndexOf, Insert, RemoveAt等等。我們可以這樣說,IEnumerable支援的功能最少,只有遍歷。而ICollection支援的功能稍微多一點,不僅有遍歷還有維護這個集合的功能。而IList是最全的版本。

  IReadOnlyList<T>

  這個是在Framework4.5中新增的介面型別,可以被看作是IList<T>的縮減版,去掉了所有可能更改這個集合的功能。比如:Add, RemoveAt等等。

  IDictionary<TKey,TValue>

  IDictionary提供了對鍵值對集合的訪問,也是繼承了ICollection<T>和IEnumerable,擴充套件了通過Key來訪問和運算元據的方法。

 關聯性泛型集合類

  關聯性集合類即我們常說的鍵值對集合,允許我們通過Key來訪問和維護集合。我們先來看一下 FCL為我們提供了哪些泛型的關聯性集合類:

  1. Dictionary<TKey,TValue>
  2. SortedDictionary<TKey,TValue>
  3. SortedList<TKey,TValue>

  Dictionary<TKey,TValue>

  Dictionary<TKey,TValue>可能是我們最常用的關聯性集合了,它的訪問,新增,刪除資料所花費的時間是所有集合類裡面最快的,因為它內部用了Hashtable作為儲存結構,所以不管儲存了多少鍵值對,查詢/新增/刪除所花費的時間都是一樣的,它的時間複雜度是O(1)。

  Dictionary<TKey,TValue>優勢是查詢插入速度快,那麼什麼是它的劣勢呢?因為採用Hashtable作為儲存結構,就意味著裡面的資料是無序排列的,所以想按一定的順序去遍歷Dictionary<TKey,TValue>裡面的資料是要費一點工夫的。

  作為TKey的型別必須實現GetHashCode()Equals() 或者提供一個IEqualityComparer否則操作可能會出現問題。

  SortedDictioanry<TKey,TValue>

  SortedDictionary<TKey,TValue>和Dictionary<TKey,TValue>大致上是類似的,但是在實現方式上有一點點區別。SortedDictionary<TKey,TValue>用二叉樹作為儲存結構的。並且按key的順序排列。那麼這樣的話SortedDictionary<TKey,TValue>的TKey就必須要實現IComparable<TKey>如果想要快速查詢的同時又能很好的支援排序的話,那就使用SortedDictionary吧。

  SortedList<TKey,TValue>       

  SortedList<TKey,TValue>是另一個支援排序的關聯性集合。但是不同的地方在於,SortedList實際是將資料存儲存在陣列中的。也就是說新增和移除操作都是線性的,時間複雜度是O(n),因為操作其中的元素可能導致所有的資料移動。但是因為在查詢的時候利用了二分搜尋,所以查詢的效能會好一些,時間複雜度是O(log n)。所以推薦使用場景是這樣地:如果你想要快速查詢,又想集合按照key的順序排列,最後這個集合的操作(新增和移除)比較少的話,就是SortedList了。

 非關聯性泛型集合類

  非關聯性集合就是不用key操作的一些集合類,通常我們可以用元素本身或者下標來操作。FCL主要為我們提供了以下幾種非關聯性的泛型集合類。

  1. List<T>
  2. LinkedList<T>
  3. HashSet<T>
  4. SortedSet<T>
  5. Stack<T>
  6. Queue<T>

  List<T>

  泛型的List 類提供了不限制長度的集合型別,List在內部維護了一定長度的陣列(預設初始長度是4),當我們插入元素的長度超過4或者初始長度 的時候,會去重新建立一個新的陣列,這個新陣列的長度是初始長度的2倍(不永遠是2倍,當發現不斷的要擴充的時候,倍數會變大),然後把原來的陣列拷貝過來。所以如果知道我們將要用這個集合裝多少個元素的話,可以在建立的時候指定初始值,這樣就避免了重複的建立新陣列和拷貝值。

  另外的話由於內部實質是一個陣列,所以在List的未必新增資料是比較快的,但是如果在資料的頭或者中間新增刪除資料相對來說更低效一些因為會影響其它資料的重新排列。

  LinkedList<T>

  LinkedList在內部維護了一個雙向的連結串列,也就是說我們在LinkedList的任何位置新增或者刪除資料其效能都是很快的。因為它不會導致其它元素的移動。一般情況下List已經夠我們使用了,但是如果對這個集合在中間的新增刪除操作非常頻繁的話,就建議使用LinkedList。

  HashSet<T>

  HashSet是一個無序的能夠保持唯一性的集合。我們也可以把HashSet看作是Dictionary<TKey,TValue>,只不過TKey和TValue都指向同一個物件。HashSet非常適合在我們需要保持集合內元素唯一性但又不需要按順序排列的時候。

  HashSet不支援下標訪問。

  SortedSet<T>

  SortedSet和HashSet,就像SortedDictionary和Dictionary一樣,還記得這兩個的區別麼?SortedSet內部也是一個二叉樹,用來支援按順序的排列元素。

  Stack<T>

  後進先出的佇列
  不支援按下標訪問

  Queu<T>

  先進先出的佇列
  不支援按下標訪問

 推薦使用場景

集合

順序排列

連順儲存

直接訪問方式

訪問時間

操作時間

備註

Dictionary

 

Key

Key:

O(1)

 

O(1)

訪問效能最快,不支援排序

SortedDinctionary

順序排列

Key

Key: 
O(log n)

O(log n)

快速訪問和支援排序的折衷

SortedList

順序排列

Key

Key:

O(log n)

 

O(n)

和SortedDictionary相似,只是內部用資料替代樹作為儲存結構。

List

使用者可以精確控制元素的位置

Index

Index: O(1)

Value: O(n)

 

O(n)

最適合需要直接訪問每一個元素的少量集合。

LinkedList

使用者可以精確控制元素的位置

不支援

Value:

O(n)

 

O(1)

最適合不需要直接訪問單個元素,但是在集合中新增/移除非常頻繁的場景。

HashSet

不支援

Key

Key:

O(1)

 

O(1)

能保持元素唯一性的集合。不支援排序

SortedSet

順序排列

Key

Key:

O(log n)

 

O(log n)

能保持元素唯一性並且支援排序。

Stack

LIFO

只能獲取頂部元素

Top: O(1)

O(1)

 

Queue

FIFO

只能獲底部元素

Front: O(1)

O(1)

 

 非泛型類集合

  泛型集合類是在.NET2.0的時候出來的,也就是說在1.0的時候是沒有這麼方便的東西的。現在基本上我們已經不使用這些集合類了,除非在做一些和老程式碼保持相容的工作的時候。來看看1.0時代的.NET程式設計師們都有哪些集合類可以用。

  1. ArraryList

  後來被List<T>替代。

  1. HashTable 後來被Dictionary<TKey,TValue>替代。
  2. Queue 後來被Queue<T>替代。
  3. SortedList 後來被SortedList<T>替代。
  4. Stack 後來被Stack<T>替代。

 執行緒安全的集合類

  1. ConcurrentQueue 執行緒安全版本的Queue
  2. ConcurrentStack執行緒安全版本的Stack
  3. ConcurrentBag執行緒安全的物件集合
  4. ConcurrentDictionary執行緒安全的Dictionary
  5. BlockingCollection

  .NET為我們提供的集合類是我們很常用的工具類之一,希望這篇文章能夠幫助大家更好的認識這些集合類。當然,個人感覺還有不完善的地方,比如說HashTable和Binary Search Tree就沒有細究下去,包括單向連結串列和雙向連結串列之間的對比本文也沒有提及。感興趣的朋友可以深入瞭解一下。

相關文章