幾乎全部要學習的接口都位於System.Collections.Generic
命名空間。圖B-1展現了.NET4.5之前主要接口間的關係,此外還將非泛型的IEnumerable
做爲根接口包括了進來。爲避免圖表過於複雜,此處沒有包含.NET 4.5的只讀接口。安全
圖B-1 System.Collections.Generic中的接口(不包括.NET 4.5)markdown
正如咱們已經屢次看到的,最基礎的泛型集合接口爲IEnumerable<T>
,表示可迭代的項的序列。IEnumerable<T>
能夠請求一個IEnumerator<T>
類型的迭代器。因爲分離了可迭代序列和迭代器,這樣多個迭代器能夠同時獨立地操做同一個序列。若是從數據庫角度來考慮,表就是IEnumerable<T>
,而遊標是IEnumerator<T>
。本附錄僅有的兩個可變(variant)集合接口爲.NET 4中的IEnumerable<out T>
和IEnumerator<out T>
;其餘全部接口的元素類型值都可雙向進出,所以必須保持不變。網絡
接下來是ICollection<T>
,它擴展了IEnumerable<T>
,添加了兩個屬性(Count
和IsReadOnly
)、變更方法(Add
、Remove
和Clear
)、CopyTo
(將內容複製到數組中)和Contains
(判斷集合是否包含特殊的元素)。全部標準的泛型集合實現都實現了該接口。數據結構
IList<T>
全都是關於定位的:它提供了一個索引器、InsertAt
和RemoveAt
(分別與Add
和Remove
相同,但能夠指定位置),以及IndexOf
(判斷集合中某元素的位置)。對IList<T>
進行迭代時,返回項的索引一般爲0、1,以此類推。文檔裏沒有完整的記錄,但這是個合理的假設。一樣,一般認爲能夠快速經過索引對IList<T>
進行隨機訪問。多線程
IDictionary<TKey, TValue>
表示一個獨一無二的鍵到它所對應的值的映射。值沒必要是惟一的,並且也能夠爲空;而鍵不能爲空。能夠將字典當作是鍵/值對的集合,所以IDictionary<TKey, TValue>
擴展了ICollection<KeyValuePair<TKey, TValue>>
。獲取值能夠經過索引器或TryGetValue
方法;與非泛型IDictionary
類型不一樣,若是試圖用不存在的鍵獲取值,IDictionary<TKey, TValue>
的索引器將拋出一個KeyNotFoundException
。TryGetValue
的目的就是保證在用不存在的鍵進行探測時還能正常運行。併發
ISet<T>
是.NET 4新引入的接口,表示惟一值集。它反過來應用到了.NET 3.5中的HashSet<T>
上,以及.NET 4引入的一個新的實現——SortedSet<T>
。框架
在實現功能時,使用哪一個接口(甚至實現)是十分明顯的。難的是如何將集合做爲API的一部分公開;返回的類型越具體,調用者就越依賴於你指定類型的附加功能。這可使調用者更輕鬆,但代價是下降了實現的靈活性。我一般傾向於將接口做爲方法和屬性的返回類型,而不是保證一個特定的實現類。在API中公開易變集合以前,你也應該深思熟慮,特別是當集合表明的是對象或類型的狀態時。一般來講,返回集合的副本或只讀的包裝器是比較適宜的,除非方法的所有目的就是經過返回集合作出變更。
B.2 列表
從不少方面來講,列表是最簡單也最天然的集合類型。框架中包含不少實現,具備各類功能和性能特徵。一些經常使用的實如今哪裏均可以使用,而一些較有難度的實現則有其專門的使用場景。
B.2.1 List<T>
在大多數狀況下,List<T>
都是列表的默認選擇。它實現了IList<T>
,所以也實現了ICollection<T>
、IEnumerable<T>
和IEnumerable
。此外,它還實現了非泛型的ICollection
和IList
接口,並在必要時進行裝箱和拆箱,以及進行執行時類型檢查,以保證新元素始終與T
兼容。
List<T>
在內部保存了一個數組,它跟蹤列表的邏輯大小和後臺數組的大小。向列表中添加元素,在簡單狀況下是設置數組的下一個值,或(若是數組已經滿了)將現有內容複製到新的更大的數組中,而後再設置值。這意味着該操做的複雜度爲O(1)或O(n),取決因而否須要複製值。擴展策略沒有在文檔中指出,所以也不能保證——但在實踐中,該方法一般能夠擴充爲所需大小的兩倍。這使得向列表末尾附加項爲O(1)平攤複雜度(amortized complexity);有時耗時更多,但這種狀況會隨着列表的增長而愈來愈少。
你能夠經過獲取和設置Capacity
屬性來顯式管理後臺數組的大小。TrimExcess
方法可使容量等於當前的大小。實戰中不多有必要這麼作,但若是在建立時已經知道列表的實際大小,則可將初始的容量傳遞給構造函數,從而避免沒必要要的複製。
從List<T>
中移除元素須要複製全部的後續元素,所以其複雜度爲O(n – k),其中k爲移除元素的索引。從列表尾部移除要比從頭部移除廉價得多。另外一方面,若是要經過值移除元素而不是索引(經過Remove
而不是RemoveAt
),那麼無論元素位置如何複雜度都爲O(n):每一個元素都將獲得平等的檢查或打亂。
List<T>
中的各類方法在必定程度上扮演着LINQ前身的角色。ConvertAll
可進行列表投影;FindAll
對原始列表進行過濾,生成只包含匹配指定謂詞的值的新列表。Sort
使用類型默認的或做爲參數指定的相等比較器進行排序。但Sort
與LINQ中的OrderBy
有個顯著的不一樣:Sort
修改原始列表的內容,而不是生成一個排好序的副本。而且,Sort
是不穩定的,而OrderBy
是穩定的;使用Sort
時,原始列表中相等元素的順序可能會不一樣。LINQ不支持對List<T>
進行二進制搜索:若是列表已經按值正確排序了,BinarySearch
方法將比線性的IndexOf
搜索效率更高( 二進制搜索的複雜度爲O(log n),線性搜索爲O(n))。
List<T>
中略有爭議的部分是ForEach
方法。顧名思義,它遍歷一個列表,並對每一個值都執行某個委託(指定爲方法的參數)。不少開發者要求將其做爲IEnumerable<T>
的擴展方法,但卻一直沒能如願;Eric Lippert在其博客中講述了這樣作會致使哲學麻煩的緣由(參見http://mng.bz/Rur2)。在我看來使用Lambda表達式調用ForEach
有些矯枉過正。另外一方面,若是你已經擁有一個要爲列表中每一個元素都執行一遍的委託,那還不如使用ForEach
,由於它已經存在了。
B.2.2 數組
在某種程度上,數組是.NET中最低級的集合。全部數組都直接派生自System.Array
,也是惟一的CLR直接支持的集合。一維數組實現了IList<T>
(及其擴展的接口)和非泛型的IList
、ICollection
接口;矩形數組只支持非泛型接口。數組從元素角度來講是易變的,從大小角度來講是固定的。它們顯示實現了集合接口中全部的可變方法(如Add
和Remove
),並拋出NotSupportedException
。
引用類型的數組一般是協變的;如Stream[]
引用能夠隱式轉換爲Object[]
,而且存在顯式的反向轉換(容易混淆的是,也能夠將Stream[]
隱式轉換爲IList<Object>
,儘管IList<T>
自己是不變的)。這意味着將在執行時驗證數組的改變——數組自己知道是什麼類型,所以若是先將Stream[]
數組轉換爲Object[]
,而後再試圖向其存儲一個非Stream
的引用,則將拋出ArrayTypeMismatchException
。
CLR包含兩種不一樣風格的數組。向量是下限爲0的一維數組,其他的統稱爲數組(array)。向量的性能更佳,是C#中最經常使用的。T[][]
形式的數組仍然爲向量,只不過元素類型爲T[]
;只有C#中的矩形數組,如string[10, 20]
,屬於CLR術語中的數組。在C#中,你不能直接建立非零下限的數組——須要使用Array.CreateInstance
來建立,它能夠分別指定下限、長度和元素類型。若是建立了非零下限的一維數組,就沒法將其成功轉換爲T[]
——這種強制轉換能夠經過編譯,但會在執行時失敗。
C#編譯器在不少方面都內嵌了對數組的支持。它不只知道如何建立數組及其索引,還能夠在foreach
循環中直接支持它們;在使用表達式對編譯時已知爲數組的類型進行迭代時,將使用Length
屬性和數組索引器,而不會建立迭代器對象。這更高效,但性能上的區別一般忽略不計。
與List<T>
相同,數組支持ConvertAll
、FindAll
和BinarySearch
方法,不過對數組來講,這些都是Array
類的以數組爲第一個參數的靜態方法。
回到本節最開始所說的,數組是至關低級的數據結構。它們是其餘集合的重要根基,在適當的狀況下有效,但在大量使用以前仍是應該三思。Eric一樣爲該話題撰寫了博客,指出它們有「些許害處」(參見http://mng.bz/3jd5)。我不想誇大這一點,但在選擇數組做爲集合類型時,這是一個值得注意的缺點。
B.2.3 LinkedList<T>
何時列表不是list呢?答案是當它爲鏈表的時候。LinkedList<T>
在不少方面都是一個列表,特別的,它是一個保持項添加順序的集合——但它卻沒有實現IList<T>
。由於它沒法聽從經過索引進行訪問的隱式契約。它是經典的計算機科學中的雙向鏈表:包含頭節點和尾節點,每一個節點都包含對鏈表中前一個節點和後一個節點的引用。每一個節點都公開爲一個LinkedListNode<T>
,這樣就能夠很方便地在鏈表的中部插入或移除節點。鏈表顯式地維護其大小,所以能夠訪問Count
屬性。
在空間方面,鏈表比維護後臺數組的列表效率要低,同時它還不支持索引操做,但在鏈表中的任意位置插入或移除元素則很是快,前提是隻要在相關位置存在對該節點的引用。這些操做的複雜度爲O(1),由於所須要的只是對周圍的節點修改前/後的引用。插入或移除頭尾節點屬於特殊狀況,一般能夠快速訪問須要修改的節點。迭代(向前或向後)也是有效的,只須要按引用鏈的順序便可。
儘管LinkedList<T>
實現了Add
等標準方法(向鏈表末尾添加節點),我仍是建議使用顯式的AddFirst
和AddLast
方法,這樣可使意圖更清晰。它還包含匹配的RemoveFirst
和RemoveLast
方法,以及First
和Last
屬性。全部這些操做返回的都是鏈表中的節點而不是節點的值;若是鏈表是空(empty)的,這些屬性將返回空(null)。
B.2.4 Collection<T>
、BindingList<T>
、ObservableCollection<T>
和 KeyedCollection<TKey, TItem>
Collection<T>
與咱們將要介紹的剩餘列表同樣,位於System.Collections.ObjectModel
命名空間。與List<T>
相似,它也實現了泛型和非泛型的集合接口。
儘管你能夠對其自身使用Collection<T>
,但它更常見的用法是做爲基類使用。它常扮演其餘列表的包裝器的角色:要麼在構造函數中指定一個列表,要麼在後臺新建一個List<T>
。全部對於集合的變更行爲,都經過受保護的虛方法(InsertItem
、SetItem
、RemoveItem
和ClearItems
)實現。派生類能夠攔截這些方法,引起事件或提供其餘自定義行爲。派生類可經過Items
屬性訪問被包裝的列表。若是該列表爲只讀,公共的變更方法將拋出異常,而再也不調用虛方法,你沒必要在覆蓋的時候再次檢查。
BindingList<T>
和ObservableCollection<T>
派生自Collection<T>
,能夠提供綁定功能。BindingList<T>
在.NET 2.0中就存在了,而ObservableCollection<T>
是WPF(Windows Presentation Foundation)引入的。固然,在用戶界面綁定數據時沒有必要必定使用它們——你也許有本身的理由,對列表的變化更有興趣。這時,你應該觀察哪一個集合以更有用的方式提供了通知,而後再選擇使用哪一個。注意,只會通知你經過包裝器所發生的變化;若是基礎列表被其餘可能會修改它的代碼共享,包裝器將不會引起任何事件。
KeyedCollection<TKey, TItem>
是列表和字典的混合產物,能夠經過鍵或索引來獲取項。與普通字典不一樣的是,鍵不能獨立存在,應該有效地內嵌在項中。在許多狀況下,這很天然,例如一個擁有CustomerID
屬性的Customer
類型。KeyedCollection<,>
爲抽象類;派生類將實現GetKeyForItem
方法,能夠從列表中的任意項中提取鍵。在咱們這個客戶的示例中,GetKeyForItem
方法返回給定客戶的ID。與字典相似,鍵在集合中必須是惟一的——試圖添加具備相同鍵的另外一個項將失敗並拋出異常。儘管不容許空鍵,但GetKeyForItem
能夠返回空(若是鍵類型爲引用類型),這時將忽略鍵(而且沒法經過鍵獲取項)。
B.2.5 ReadOnlyCollection<T>
和ReadOnlyObservableCollection<T>
最後兩個列表更像是包裝器,即便基礎列表爲易變的也只提供只讀訪問。它們仍然實現了泛型和非泛型的集合接口。而且混合使用了顯式和隱式的接口實現,這樣使用具體類型的編譯時表達式的調用者將沒法使用變更操做。
ReadOnlyObservableCollection<T>
派生自ReadOnlyCollection<T>
,並和ObserverbleCollection<T>
同樣實現了相同的INotifyCollectionChanged
和INotifyPropertyChanged
接口。ReadOnlyObservableCollection<T>
的實例只能經過一個ObservableCollection<T>
後臺列表進行構建。儘管集合對調用者來講依然是隻讀的,但它們能夠觀察對後臺列表其餘地方的改變。
儘管一般狀況下我建議使用接口做爲API中方法的返回值,但特地公開ReadOnlyCollection<T>
也是頗有用的,它能夠爲調用者清楚地指明不能修改返回的集合。但仍需寫明基礎集合是否能夠在其餘地方修改,或是否爲有效的常量。
B.3 字典
在框架中,字典的選擇要比列表少得多。只有三個主流的非併發IDictionary<TKey, TValue>
實現,此外還有ExpandoObject
(第14章已介紹過)、ConcurrentDictionary
(將在介紹其餘併發集合時介紹)和RouteValueDictionary
(用於路由Web請求,特別是在ASP.NET MVC中)也實現了該接口。
注意,字典的主要目的在於爲值提供有效的鍵查找。
B.3.1 Dictionary<TKey, TValue>
若是沒有特殊需求,Dictionary<TKey, TValue>
將是字典的默認選擇,就像List<T>
是列表的默認實現同樣。它使用了散列表,能夠實現有效的查找(參見http://mng.bz/qTdH),雖然這意味着字典的效率取決於散列函數的優劣。可以使用默認的散列和相等函數(調用鍵對象自己的Equals
和GetHashCode
),也能夠在構造函數中指定IEqualityComparer<TKey>
做爲參數。
最簡單的示例是用不區分大小寫的字符串鍵實現字典,如代碼清單B-1所示。
代碼清單B-1 在字典中使用自定義鍵比較器
var comparer = StringComparer.OrdinalIgnoreCase; var dict = new Dictionary<String, int>(comparer); dict["TEST"] = 10; Console.WriteLine(dict["test"]); //輸出10
儘管字典中的鍵必須惟一,但散列碼並不須要如此。兩個不等的鍵徹底有可能擁有相同的散列碼;這就是散列衝突(hash collision)(http://en.wikipedia.org/wiki/Collision_(computer_science)——譯者注),儘管這多少會下降字典的效率,但卻能夠正常工做。若是鍵是易變的,而且散列碼在插入後發生了改變,字典將會失敗。易變的字典鍵老是一個壞主意,但若是確實不得不使用,則應確保在插入後不會改變。
散列表的實現細節是沒有規定的,可能會隨時改變,但一個重要的方面可能會引發混淆:儘管Dictionary<TKey, TValue>
有時可能會按順序排列,但沒法保證老是這樣。若是向字典添加了若干項而後迭代,你會發現項的順序與插入時相同,但請不要信覺得真。有點不幸的是,刻意添加條目以維持排序的實現可能會很怪異,而碰巧天然擾亂了排序的實現則可能帶來更少的混淆。
與List<T>
同樣,Dictionary<TKey, TValue>
將條目保存在數組中,並在必要的時候進行擴充,且擴充的平攤複雜度爲O(1)。若是散列合理,經過鍵訪問的複雜度也爲O(1);而若是全部鍵的散列碼都相等,因爲要依次檢查各個鍵是否相等,所以最終的複雜度爲O(n)。在大多數實際場合中,這都不是問題。
B.3.2 SortedList<TKey, TValue>
和SortedDictionary<TKey, TValue>
乍一看可能會覺得名爲SortedList<,>
的類爲列表,但實則否則。這兩個類型都是字典,而且誰也沒有實現IList<T>
。若是取名爲ListBackedSortedDictionary
和TreeBackedSortedDictionary
可能更加貼切,但如今改已經來不及了。
這兩個類有不少共同點:比較鍵時都使用IComparer<TKey>
而不是IEqualityComparer<TKey>
,而且鍵是根據比較器排好序的。在查找值時,它們的性能均爲O(log n),而且都能執行二進制搜索。但它們的內部數據結構卻迥然不一樣:SortedList<,>
維護一個排序的條目數組,而SortedDictionary<,>
則使用的是紅黑樹結構(參見維基百科條目http://mng.bz/K1S4)。這致使了插入和移除時間以及內存效率上的顯著差別。若是要建立一個排序的字典,SortedList<,>
將被有效地填充,想象一下保持List<T>
排序的步驟,你會發現向列表末尾添加單項是廉價的(若忽略數組擴充的話將爲O(1)),而隨機添加項則是昂貴的,由於涉及複製已有項(最糟糕的狀況是O(n))。向SortedDictionary<,>
中的平衡樹添加項老是至關廉價(複雜度爲O(log n)),但在堆上會爲每一個條目分配一個樹節點,這將使開銷和內存碎片比使用SortedList<,>
鍵值條目的數組要更多。
這兩種集合都使用單獨的集合公開鍵和值,而且這兩種狀況下返回的集合都是活動的,由於它們將隨着基礎字典的改變而改變。但SortedList<,>
公開的集合實現了IList<T>
,所以可使用排序的鍵索引有效地訪問條目。
我不想由於談論了這麼多關於複雜度的內容而給你形成太大困擾。若是不是海量數據,則可沒必要擔憂所使用的實現。若是字典的條目數可能會很大,你應該仔細分析這兩種集合的性能特色,而後決定使用哪個。
B.3.3 ReadOnlyDictionary<TKey, TValue>
熟悉了B.2.5節中介紹的ReadOnlyCollection<T>
後,ReadOnlyDictionary<TKey, TValue>
應該也不會讓你感到特別意外。ReadOnlyDictionary<TKey, TValue>
也只是一個圍繞已有集合(本例中指IDictionary<TKey, TValue>
)的包裝器而已,可隱藏顯式接口實現後全部發生變化的操做,而且在調用時拋出NotSupportedException
。
與只讀列表相同,ReadOnlyDictionary<TKey, TValue>
的確只是一個包裝器;若是基礎集合(傳入構造函數的集合)發生變化,則這些修改內容可經過包裝器顯現出來。
B.4 集
在.NET 3.5以前,框架中根本沒有公開集(set)集合。若是要在.NET 2.0中表示集,一般會使用Dictionary<,>
,用集的項做爲鍵,用假數據做爲值。.NET3.5的HashSet<T>
在必定程度上改變了這一局面,如今.NET 4還添加了SortedSet<T>
和通用的ISet<T>
接口。儘管在邏輯上,集接口應該只包含Add
/Remove
/Contains
操做,但ISet<T>
還指定了不少其餘操做來控制集(ExceptWith
、IntersectWith
、SymmetricExceptWith
和UnionWith
)並在各類複雜條件下驗證集(SetEquals
、Overlaps
、IsSubsetOf
、IsSupersetOf
、IsProperSubsetOf
和IsProperSupersetOf
)。全部這些方法的參數均爲IEnumerable<T>
而不是ISet<T>
,這乍看上去會很奇怪,但卻意味着集能夠很天然地與LINQ進行交互。
B.4.1 HashSet<T>
HashSet<T>
是不含值的Dictionary<,>
。它們具備相同的性能特徵,而且你也能夠指定一個IEqualityComparer<T>
來自定義項的比較。一樣,HashSet<T>
所維護的順序也不必定就是值添加的順序。
HashSet<T>
添加了一個RemoveWhere
方法,能夠移除全部匹配給定謂詞的條目。這能夠在迭代時對集進行刪減,而沒必要擔憂在迭代時不能修改集合的禁令。
B.4.2 SortedSet<T>
(.NET 4)
就像HashSet<T>
之於Dictionary<,>
同樣,SortedSet<T>
是沒有值的SortedDictionary<,>
。它維護一個值的紅黑樹,添加、移除和包含檢查(containment check)的複雜度爲O(log n)。在對集進行迭代時,產生的是排序的值。
和HashSet<T>
同樣它也提供了RemoveWhere
方法(儘管接口中沒有),而且還提供了額外的屬性(Min
和Max
)用來返回最小和最大值。一個比較有趣的方法是GetViewBetween
,它返回介於原始集上下限以內(含上下限)的另外一個SortedSet<T>
。這是一個易變的活動視圖——對於它的改變將反映到原始集上,反之亦然,如代碼清單B-2所示。
代碼清單B-2 經過視圖觀察排序集中的改變
var baseSet = new SortedSet<int> { 1, 5, 12, 20, 25 }; var view = baseSet.GetViewBetween(10, 20); view.Add(14); Console.WriteLine(baseSet.Count); //輸出6
foreach (int value in view) { Console.WriteLine(value); //輸出十二、1四、20
}
儘管GetViewBetween
很方便,卻不是免費的午飯:爲保持內部的一致性,對視圖的操做可能比預期的更昂貴。尤爲在訪問視圖的Count
屬性時,若是在上次遍歷以後基礎集發生了改變,操做的複雜度將爲O(n)。全部強大的工具,都應該謹慎用之。
SortedSet<T>
的最後一個特性是它公開了一個Reverse()
方法,能夠進行反序迭代。Enumerable.Reverse()
沒有使用該方法,而是緩衝了它調用的序列的內容。若是你知道要反序訪問排序集,使用SortedSet<T>
類型的表達式代替更通用的接口類型可能會更有用,由於可訪問這個更高效的實現。
B.5 Queue<T>和Stack<T>
隊列和棧是全部計算機科學課程的重要組成部分。它們有時分別指FIFO(先進先出)和LIFO(後進先出)結構。這兩種數據結構的基本理念是相同的:向集合添加項,並在其餘時候移除。所不一樣的是移除的順序:隊列就像排隊進商店,排在第一位的將是第一個被接待的;棧就像一摞盤子,最後一個放在頂上的將是最早被取走的。隊列和棧的一個常見用途是維護一個待處理的工做項清單。
正如LinkedList<T>
同樣,儘管可以使用普通的集合接口方法來訪問隊列和棧,但我仍是建議使用指定的類,這樣代碼會更加清晰。
B.5.1 Queue<T>
Queue<T>
實現爲一個環形緩衝區:本質上它維護一個數組,包含兩個索引,分別用於記住下一個添加項和取出項的位置(slot)。若是添加索引追上了移除索引,全部內容將被複制到一個更大的數組中。
Queue<T>
提供了Enqueue
和Dequeue
方法,用於添加和移除項。Peek
方法用來查看下一個出隊的項,而不會實際移除。Dequeue
和Peek
在操做空(empty)隊列時都將拋出InvalidOperationException
。對隊列進行迭代時,產生的值的順序與出隊時一致。
B.5.2 Stack<T>
Stack<T>
的實現比Queue<T>
還簡單——你能夠把它想成是一個List<T>
,只不過它還包含Push
方法用於向列表末尾添加新項,Pop
方法用於移除最後的項,以及Peek
方法用於查看而不移除最後的項。一樣,Pop
和Peek
在操做空(empty)棧時將拋出InvalidOperationException
。對棧進行迭代時,產生的值的順序與出棧時一致——即最近添加的值將率先返回。
B.6 並行集合(.NET 4)
做爲.NET 4並行擴展的一部分,新的System.Collections.Concurrent
命名空間中包含一些新的集合。它們被設計爲在含有較少鎖的多線程併發操做時是安全的。該命名空間下還包含三個用於對併發操做的集合進行分區的類,但在此咱們不討論它們。
B.6.1 IProducerConsumerCollection<T>
和BlockingCollection<T>
IProducerConsumerCollection<T>
被設計用於BlockingCollection<T>
,有三個新的集合實現了該接口。在描述隊列和棧時,我說過它們一般用於爲稍後的處理存儲工做項;生產者/消費者模式是一種並行執行這些工做項的方式。有時只有一個生產者線程建立工做,多個消費者線程執行工做項。在其餘狀況下,消費者也能夠是生產者,例如,網絡爬蟲(crawler)處理一個Web頁面時會發現更多的連接,供後續爬取。
IProducerConsumerCollection<T>
是生產者/消費者模式中數據存儲的抽象,BlockingCollection<T>
以易用的方式包裝該抽象,並提供了限制一次緩衝多少項的功能。BlockingCollection<T>
假設沒有東西會直接添加到包裝的集合中,全部相關方都應該使用包裝器來對工做項進行添加和移除。構造函數包含一個重載,不傳入IProducerConsumerCollection<T>
參數,而使用ConcurrentQueue<T>
做爲後臺存儲。
IProducerConsumerCollection<T>
只提供了三個特別有趣的方法:ToArray
、TryAdd
和TryTake
。ToArray
將當前集合內容複製到新的數組中,這個數組是集合在調用該方法時的快照。TryAdd
和TryTake
都遵循了標準的TryXXX
模式,試圖向集合添加或移除項,返回指明成功或失敗的布爾值。它容許有效的失敗模式,下降了對鎖的需求。例如在Queue<T>
中,要把「驗證隊列中是否有項」和「若是有項就進行出隊操做」這兩個操做合併爲一個,就須要一個鎖——不然Dequeue
就可能拋出異常(例如,當隊列有且僅有一個項時,兩個線程同時判斷它是否有項,而且都返回true,這時其中一個線程先執行了出隊操做,而另外一個線程再執行出隊操做時,因爲隊列已經空了,所以將拋出異常。——譯者注)。
BlockingCollection<T>
包含一系列重載,容許指定超時和取消標記,能夠在這些非阻塞方法之上提供阻塞行爲。一般不須要直接使用BlockingCollection<T>
或IProducerConsumerCollection<T>
,你能夠調用並行擴展中使用了這兩個類的其餘部分。但瞭解它們仍是頗有必要的,特別是在須要自定義行爲的時候。
B.6.2 ConcurrentBag<T>
、ConcurrentQueue<T>
和ConcurrentStack<T>
框架自帶了三個IProducerConsumerCollection<T>
的實現。本質上,它們在獲取項的順序上有所不一樣;隊列和棧與它們非併發等價類的行爲一致,而ConcurrentBag<T>
沒有順序保證。
它們都以線程安全的方式實現了IEnumerable<T>
。GetEnumerator()
返回的迭代器將對集合的快照進行迭代;迭代時能夠修改集合,而且改變不會出如今迭代器中。這三個類都提供了與TryTake
相似的TryPeek
方法,不過不會從集合中移除值。與TryTake
不一樣的是,IProducerConsumerCollection<T>
中沒有指定TryPeek
方法。
B.6.3 ConcurrentDictionary<TKey, TValue>
ConcurrentDictionary<TKey, TValue>
實現了標準的IDictionary<TKey, TValue>
接口(可是全部的併發集合沒有一個實現了IList<T>
),本質上是一個線程安全的基於散列的字典。它支持併發的多線程讀寫和線程安全的迭代,不過與上節的三個集合不一樣,在迭代時對字典的修改,可能會也可能不會反映到迭代器上。
它不只僅意味着線程安全的訪問。普通的字典實現基本上能夠經過索引器提供添加或更新,經過Add
方法添加或拋出異常,但ConcurrentDictionary<TKey, TValue>
提供了名副其實的大雜燴。你能夠根據前一個值來更新與鍵關聯的值;經過鍵獲取值,若是該鍵事先不存在就添加;只有在值是你所指望的時候纔有條件地更新;以及許多其餘的可能性,全部這些行爲都是原子的。在開始時都顯得很難,但並行團隊的Stephen Toub撰寫了一篇博客,詳細介紹了何時應該使用哪個方法(參見http://mng.bz/WMdW)。
B.7 只讀接口(.NET 4.5)
NET 4.5引入了三個新的集合接口,即IReadOnlyCollection<T>
、IReadOnlyList<T>
和IReadOnlyDictionary<TKey, TValue>
。截至本書撰寫之時,這些接口尚未獲得普遍應用。儘管如此,仍是有必要了解一下的,以便知道它們不是什麼。圖B-2展現了三個接口間以及和IEnumerable
接口的關係。
圖B-2 .NET 4.5的只讀接口
若是以爲ReadOnlyCollection<T>
的名字有點言過其實,那麼這些接口則更加詭異。它們不只容許其餘代碼對其進行修改,並且若是集合是可變的,甚至能夠經過結合對象自己進行修改。例如,List<T>
實現了IReadOnlyList<T>
,但顯然它並非一個只讀集合。
固然這並非說這些接口沒有用處。IReadOnlyCollection<T>
和IReadOnlyList<T>
對於T
都是協變的,這與IEnumerable<T>
相似,但還暴露了更多的操做。惋惜IReadOnlyDictionary<TKey, TValue>
對於兩個類型參數都是不變的,由於它實現了IEnumerable<KeyValuePair<TKey, TValue>>
,而KeyValuePair<TKey, TValue>
是一個結構,自己就是不變的。此外,IReadOnlyList<T>
的協變性意味着它不能暴露任何以T
爲參數的方法,如Contains
和IndexOf
。其最大的好處在於它暴露了一個索引器,經過索引來獲取項。
目前我並沒怎麼使用過這些接口,但我相信它們在將來確定會發揮重要做用。2012年末,微軟在NuGet上發佈了不可變集合的預覽版,即Microsoft.Bcl.Immutable
。BCL團隊的博客文章(http://mng.bz/Xlqd)道出了更多細節,不過它基本上無需解釋:不可變的集合和可凍結的集合(可變集合,在凍結後變爲不可變集合)。固然,若是元素類型是可變的(如StringBuilder
),那它也只能幫你到這了。但我依然爲此興奮不已,由於不可變性實在是太有用了。
B.8 小結
.NET Framework包含一系列豐富的集合(儘管對於集來講沒那麼豐富)(做者前面使用了a rich set of collecions,後面用了a rich collection of sets,分別表示豐富的集合和集。此處的中文沒法體現原文這種對仗。——譯者注)。它們隨着框架的其餘部分一塊兒逐漸成長起來,儘管接下來的一段時間內,最經常使用的集合還應該是List<T>
和Dictionary<TKey, TValue>
。
固然將來還會有其餘數據結構添加進來,但要在其好處與添加到核心框架中的代價之間作出權衡。也許將來咱們會看到明確的基於樹的API,而不是像如今這樣使用樹做爲已有集合的實現細節。也許能夠看到斐波納契堆(Fibonacci heaps)、弱引用緩存等——但正如咱們所看到的那樣,對於開發者來講已經夠多了,而且有信息過載的風險。
若是你的項目須要特殊的數據結構,能夠上網找找開源實現;Wintellect的Power Collections做爲內置集合的替代品,已經有很長的歷史了(參見http://powercollections.codeplex.com)。但在大多數狀況下,框架徹底能夠知足你的需求,但願本附錄能夠在創造性使用泛型集合方面擴展你的視野。
下班回來以前對本身說:今天必定要把這篇博客寫了!然而回來之後,看看這瞅瞅那,點開各類超連接,拖到很晚纔開始,哎,這習慣真的很差。。。。