C#集合類型大揭秘
集合是.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
foreach是怎麽實現的?
for依賴對 Length 屬性和索引運算符 ([]) 的支持。借助 Length 屬性,C# 編譯器可以使用 for 語句叠代數組中的每個元素。for適用於長度固定且始終支持索引運算符的數組,但並不是所有類型集合的元素數量都是已知的。此外,許多集合類(包括 Stack
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
主要擴展的功能有:
- 新增了屬性Count,用於記錄集合元素個數
- 支持添加元素和移除元素
- 支持是否包含某元素
- 支持清空集合等等
對於任何實現了ICollection
IList和IList
IList
主要擴展的功能有:
- 通過索引獲取集合中某個元素
- 通過元素獲取元素在集合中的索引值
- 通過索引插入元素到集合指定位置
- 移除集合指定索引處的元素
IDictionary和IDictionary
IDictionary
主要擴展的功能有:
- 通過鍵KEY獲取值VALUE
- 插入新的鍵值對{KEY:VALUE}
- 是否包含KEY
- 通過KEY移除鍵值對元素
主要的集合的接口介紹完了,下面我們來看一下具體的集合類型。
關聯性泛型集合類
1.Dictionary
Dictionary
Dictionary
Dictionary
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
我們可以通過Dictionary
Dictionary
Dictionary
2.SortedDictionary
SortedDictionary
SortedDictionary
3.SortedList
在既需要快速查找又需要順序排列的場景下,Dictionary
SortedList
SortedList
內部實現結構:
根據Key獲取Value的實現:
IndexOfKey實現:
添加新元素:
添加操作:
非關聯性泛型集合類
1.List
泛型的List 類提供了不限制長度的集合類型,List內部實現使用數據結構是數組。我們都知道數組是長度固定的,那麽List不限制長度必定需要維護這個數組。實際上List維護了一定長度的數組(默認為4),當插入元素的個數超過4或初始長度時,會去重新創建一個新的數組,這個新數組的長度是初始長度的2倍,然後將原來的數組賦值到新的數組中。
我們可以通過ILSpy看一下List
List
新增元素操作:
新增元素確認數組容量:
真正的數組擴容操作:
數組擴容的場景涉及到對象的創建和賦值,是比較消耗性能的。所以如果能指定一個合適的初始長度,能避免頻繁的對象創建和賦值。再者,因為內部的數據結構是數組,插入和刪除操作需要移動元素位置,所以不適合頻繁的進行插入和刪除操作;但是可以通過數組下標查找元素。所以List
2.LinkedList
上面我們提到List
因為內部實現結構是鏈表,所以可以在某一個節點前或節點後插入新的元素。
鏈表節點定義:
我們以在某個節點前插入新元素為例:
具體的插入操作,註意操作步驟不能顛倒:
3.HashSet
HashSet
內部實現數據結構:
m_slots中所存放的是Slot結構體,Slot結構體由3個部分組成,如下所示:
添加新元素的具體實現:
和Dictionary
4.SortedSet
SortedSet
5.Stack
棧是一種後進先出的結構,C#的棧是借助數組實現的,考慮到棧後進先出的特性,使用數組來實現貌似是水到渠成的事。
入棧操作:
彈棧操作:
6.Queue
隊列是一種先進先出的結構,C#的隊列也是借助數組實現的,有了前面的經驗,借助數組實現必然會有數組擴容。C#的隊列實現其實是循環隊列的方式,可以簡單的理解為將隊列的頭尾相接。至於為什麽要這麽做?為了節省存儲空間和減少元素的移動。因為元素出隊列時後面的元素跟著前移是非常消耗性能的,但是不跟著向前移動的話,前面就會一直存在空閑的空間浪費內存。所以使用循環隊列來解決這種問題。
入隊操作:
出隊操作:
線程安全的集合類
需要我們註意的是,上面我們所介紹的集合並不是線程安全的,在多線程環境下,可能會出現線程安全問題。在多線程讀的情況下,我們使用普通集合即可。在多線程添加/更新/刪除時,我們可以采用手動鎖定的方式確保線程安全,但是應該註意加鎖的範圍和粒度,加鎖不當可能會導致程序性能低下甚至產生死鎖。
更好的選擇的是使用的C#提供的線程安全集合(命名空間:System.Collections.Concurrent)。線程安全集合使用幾種算法來最小化線程阻塞。
- ConcurrentQueue: 線程安全版本的Queue
- ConcurrentStack:線程安全版本的Stack
- ConcurrentBag:線程安全的對象集合
- 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#集合類型大揭秘