.NET中的泛型集合總結

最近對集合相關的命名空間比較感興趣,之前也就用下List<T>, Dictionary<Tkey, TValue>之類,總之,比較小白。點開N多博客,MSDN,StackOverflow,沒找到令我徹底滿意的答案,本打算本身總結下寫出來,工做量好大的感受……直到昨晚隨意翻到看了一些又放下的《深刻理解C#》-附錄B部分,高興地簡直要叫出來——「這總結真是太絕了,好書不愧是好書」。真是「踏破鐵鞋無覓處,得來全不費工夫」,最好的資源就在眼下,而本身竟然渾然不知。或許只有深刻技術細節的時候,才能認識到經典爲何經典吧!言歸正傳,本博客主要是對《深刻理解C#》-附錄B的摘錄,並加了些標註。數據庫

 

附錄B .NET中的泛型集合

.NET中包含不少泛型集合,而且隨着時間的推移列表還在增加。本附錄涵蓋了最重要的泛型集合接口和類,但不會涉及System.CollectionsSystem.Collections.SpecializedSystem.ComponentModel中的非泛型集合。一樣,也不會涉及ILookup<TKey,TValue>這樣的LINQ接口。本附錄是參考而非指南——在寫代碼時,能夠用它來替代MSDN。在大多數狀況下,MSDN顯然會提供更詳細的內容,但這裏的目的是在選擇代碼中要用的特定集合時,能夠快速瀏覽不一樣的接口和可用的實現。數組

我沒有指出各集合是否爲線程安全,MSDN中有更詳細的信息。普通的集合都不支持多重併發寫操做;有些支持單線程寫和併發讀操做。B.6節列出了.NET 4中添加的併發集合。此外,B.7節介紹了.NET4.5中引入的只讀集合接口。緩存

 

B.1 接口

幾乎全部要學習的接口都位於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>,添加了兩個屬性(CountIsReadOnly)、變更方法(AddRemoveClear)、CopyTo(將內容複製到數組中)和Contains(判斷集合是否包含特殊的元素)。全部標準的泛型集合實現都實現了該接口數據結構

IList<T>全都是關於定位的:它提供了一個索引器InsertAtRemoveAt(分別與AddRemove相同,但能夠指定位置),以及IndexOf(判斷集合中某元素的位置)。對IList<T>進行迭代時,返回項的索引一般爲0、1,以此類推。文檔裏沒有完整的記錄,但這是個合理的假設。一樣,一般認爲能夠快速經過索引對IList<T>進行隨機訪問。多線程

IDictionary<TKey, TValue>表示一個獨一無二的鍵到它所對應的值的映射。值沒必要是惟一的,並且也能夠爲空;而鍵不能爲空。能夠將字典當作是鍵/值對的集合,所以IDictionary<TKey, TValue>擴展了ICollection<KeyValuePair<TKey, TValue>>。獲取值能夠經過索引器或TryGetValue方法;與非泛型IDictionary類型不一樣,若是試圖用不存在的鍵獲取值,IDictionary<TKey, TValue>的索引器將拋出一個KeyNotFoundExceptionTryGetValue的目的就是保證在用不存在的鍵進行探測時還能正常運行併發

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。此外,它還實現了非泛型的ICollectionIList接口,並在必要時進行裝箱和拆箱,以及進行執行時類型檢查,以保證新元素始終與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>(及其擴展的接口)和非泛型的IListICollection接口;矩形數組只支持非泛型接口。數組從元素角度來講是易變的,從大小角度來講是固定的。它們顯示實現了集合接口中全部的可變方法(如AddRemove),並拋出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>相同,數組支持ConvertAllFindAllBinarySearch方法,不過對數組來講,這些都是Array類的以數組爲第一個參數的靜態方法。

回到本節最開始所說的,數組是至關低級的數據結構。它們是其餘集合的重要根基,在適當的狀況下有效,但在大量使用以前仍是應該三思。Eric一樣爲該話題撰寫了博客,指出它們有「些許害處」(參見http://mng.bz/3jd5)。我不想誇大這一點,但在選擇數組做爲集合類型時,這是一個值得注意的缺點。

B.2.3 LinkedList<T>

何時列表不是list呢?答案是當它爲鏈表的時候。LinkedList<T>在不少方面都是一個列表,特別的,它是一個保持項添加順序的集合——但它卻沒有實現IList<T>。由於它沒法聽從經過索引進行訪問的隱式契約。它是經典的計算機科學中的雙向鏈表:包含頭節點和尾節點,每一個節點都包含對鏈表中前一個節點和後一個節點的引用。每一個節點都公開爲一個LinkedListNode<T>,這樣就能夠很方便地在鏈表的中部插入或移除節點。鏈表顯式地維護其大小,所以能夠訪問Count屬性。

在空間方面,鏈表比維護後臺數組的列表效率要低,同時它還不支持索引操做,但在鏈表中的任意位置插入或移除元素則很是快,前提是隻要在相關位置存在對該節點的引用。這些操做的複雜度爲O(1),由於所須要的只是對周圍的節點修改前/後的引用。插入或移除頭尾節點屬於特殊狀況,一般能夠快速訪問須要修改的節點。迭代(向前或向後)也是有效的,只須要按引用鏈的順序便可

儘管LinkedList<T>實現了Add等標準方法(向鏈表末尾添加節點),我仍是建議使用顯式的AddFirstAddLast方法,這樣可使意圖更清晰。它還包含匹配的RemoveFirstRemoveLast方法,以及FirstLast屬性。全部這些操做返回的都是鏈表中的節點而不是節點的值;若是鏈表是空(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>全部對於集合的變更行爲,都經過受保護的虛方法(InsertItemSetItemRemoveItemClearItems)實現。派生類能夠攔截這些方法,引起事件或提供其餘自定義行爲。派生類可經過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>同樣實現了相同的INotifyCollectionChangedINotifyPropertyChanged接口。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),雖然這意味着字典的效率取決於散列函數的優劣。可以使用默認的散列和相等函數(調用鍵對象自己的EqualsGetHashCode),也能夠在構造函數中指定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>。若是取名爲ListBackedSortedDictionaryTreeBackedSortedDictionary可能更加貼切,但如今改已經來不及了。

這兩個類有不少共同點:比較鍵時都使用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>還指定了不少其餘操做來控制集(ExceptWithIntersectWithSymmetricExceptWithUnionWith)並在各類複雜條件下驗證集(SetEqualsOverlapsIsSubsetOfIsSupersetOfIsProperSubsetOfIsProperSupersetOf)。全部這些方法的參數均爲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方法(儘管接口中沒有),而且還提供了額外的屬性(MinMax)用來返回最小和最大值。一個比較有趣的方法是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>提供了EnqueueDequeue方法,用於添加和移除項。Peek方法用來查看下一個出隊的項,而不會實際移除。DequeuePeek在操做空(empty)隊列時都將拋出InvalidOperationException。對隊列進行迭代時,產生的值的順序與出隊時一致。

B.5.2 Stack<T>

Stack<T>的實現比Queue<T>還簡單——你能夠把它想成是一個List<T>,只不過它還包含Push方法用於向列表末尾添加新項,Pop方法用於移除最後的項,以及Peek方法用於查看而不移除最後的項。一樣,PopPeek在操做空(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>只提供了三個特別有趣的方法:ToArrayTryAddTryTakeToArray將當前集合內容複製到新的數組中,這個數組是集合在調用該方法時的快照。TryAddTryTake都遵循了標準的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爲參數的方法,如ContainsIndexOf。其最大的好處在於它暴露了一個索引器,經過索引來獲取項。

目前我並沒怎麼使用過這些接口,但我相信它們在將來確定會發揮重要做用。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)。但在大多數狀況下,框架徹底能夠知足你的需求,但願本附錄能夠在創造性使用泛型集合方面擴展你的視野。

 

下班回來以前對本身說:今天必定要把這篇博客寫了!然而回來之後,看看這瞅瞅那,點開各類超連接,拖到很晚纔開始,哎,這習慣真的很差。。。。

相關文章
相關標籤/搜索