最近对集合相关的命名空间比较感兴趣,以前也就用下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)

正如我们已经多次看到的,最基础的泛型集合接口为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"] = ;
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> { , , , ,  };
var view = baseSet.GetViewBetween(, );
view.Add();
Console.WriteLine(baseSet.Count); //输出6
foreach (int value in view)
{
Console.WriteLine(value); //输出12、14、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)。但在大多数情况下,框架完全可以满足你的需求,希望本附录可以在创造性使用泛型集合方面扩展你的视野。

下班回来之前对自己说:今天一定要把这篇博客写了!然而回来以后,看看这瞅瞅那,点开各种超链接,拖到很晚才开始,哎,这习惯真的不好。。。。

.NET中的泛型集合总结的更多相关文章

  1. C#中Dictionary泛型集合7种常见的用法

    要使用Dictionary集合,需要导入C#泛型命名空间 System.Collections.Generic(程序集:mscorlib)  Dictionary的描述1.从一组键(Key)到一组值( ...

  2. 在Jersey中如何处理泛型集合

    Jersey是一个标准的Restful Web service框架,可以方便的实现Restful的Server端和客户端. 本文主要介绍使用Jersey客户端时如何将Json格式的数组转换成java的 ...

  3. 快速入门系列--CLR--03泛型集合

    .NET中的泛型集合 在这里主要介绍常见的泛型集合,很多时候其并发时的线程安全性常常令我们担忧.因而简述下.NET并发时线程安全特性,其详情请见MSDN. 普通集合都不支持多重并发写操作 部分支持单线 ...

  4. 编写高质量代码改善C#程序的157个建议[泛型集合、选择集合、集合的安全]

    前言   软件开发过程中,不可避免会用到集合,C#中的集合表现为数组和若干集合类.不管是数组还是集合类,它们都有各自的优缺点.如何使用好集合是我们在开发过程中必须掌握的技巧.不要小看这些技巧,一旦在开 ...

  5. C#程序编写高质量代码改善的157个建议【20-22】[泛型集合、选择集合、集合的安全]

    建议20.使用泛型集合来替代非泛型集合 http://www.cnblogs.com/aehyok/p/3384637.html 这里有一篇文章,是我之前专门来介绍泛型的.我们应尽量的使用泛型集合.因 ...

  6. 计算字符串中每种字符出现的次数[Dictionary<char,int>泛型集合用法]

    有一道经典的面试题: 统计 welcome to china中每个字符出现的次数,不考虑大小写.      第一个出现在脑海里的想法是: 1. 将字字符串转换成 char数组: 2. 用 for循环遍 ...

  7. C#中的泛型和泛型集合

    泛型 泛型引入了一个概念:类型参数.通过使用类型参数(T)减少了运行时强制转换或装箱操作的风险,通过泛型可以最大限度的重用代码,保护类型的安全及提高性能,他的最常见应用就是创建集合类,可以约束集合类中 ...

  8. C# 找出泛型集合中的满足一定条件的元素 List.Wher()

    在学习的过程中,发现泛型集合List<T>有一个Where函数可以筛选出满足一定条件的元素,结合Lambda表达式使用特别方便,写出来与大家分享. 1.关于Func<> Fun ...

  9. java中数组、list、泛型集合的长度

    1 java中的length属性是针对数组说的,比如说你声明了一个数组,想知道这个数组的长度则用到了length这个属性. 2 java中的length()方法是针对字符串String说的,如果想看这 ...

随机推荐

  1. Lua中__index和__newindex实践

    [具有默认值的table] 我们都知道,table中的任何字段的默认值都是nil,但是通过元表,我们可以很容易的修改这一规定,代码如下: function setDefault(tb, default ...

  2. PHP生成HTML文件, SummerHtml

    2018-6-27 20:13:04 星期三 作用: 用PHP生成HTML文档, 支持标签嵌套缩进 起因: 这个东西确实也是心血来潮写的, 我很满意里边的实现缩进的机制, 大家有用到的可以看看 现在都 ...

  3. Linux -- Xshell ,Xftp远程连接中文乱码怎么解决?

    ### 这几天开始捣鼓lnmp的环境搭建,很多东西还是得自己去经历,才会印象深刻,有所体会,有所收获与成长! 但是,偶尔会遇到一些意想不到问题! Xshell ,Xftp 远程连接的时候出现中文乱码的 ...

  4. 【原创】大叔问题定位分享(10)提交spark任务偶尔报错 org.apache.spark.SparkException: A master URL must be set in your configuration

    spark 2.1.1 一 问题重现 问题代码示例 object MethodPositionTest { val sparkConf = new SparkConf().setAppName(&qu ...

  5. bootstrap轮播图 两侧半透明阴影

    用bootstrap轮播图:Carousel插件,图片两侧影音实在碍眼,想去掉,首先发现有css里由opacity: 0.5这个东西来控制,全部改成opacity: 0.0,发现指示箭头也看不见了. ...

  6. JAVA ArrayList实现随机生成数字,并把偶数放入一个列表中

    package Code429; import java.util.ArrayList;import java.util.Random; public class CodeArrayListPrint ...

  7. Spring设置定时任务时,关于执行时间的规则设置

    been需要在xml文件中进行配置 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE bean ...

  8. Java Spring Boot VS .NetCore (一)来一个简单的 Hello World

    系列文章 Java Spring Boot VS .NetCore (一)来一个简单的 Hello World Java Spring Boot VS .NetCore (二)实现一个过滤器Filte ...

  9. Redis常见面试题

    介绍:Redis 是一个开源的使用 ANSI C 语言编写.遵守 BSD 协议.支持网络.可基于内存亦可持久化的日志型.Key-Value 数据库,并提供多种语言的 API的非关系型数据库. 传统数据 ...

  10. mybatis代码生成器——MyBatis Generator

    1.maven依赖 a.加入依赖 <!-- mybatis生成工具 --> <dependency> <groupId>org.mybatis.generator& ...