集合
选择集合
一般情况下,应使用泛型集合。 下表介绍了一些常用的集合方案和可用于这些方案的集合类。 如果你是使用泛型集合的新手,此表将帮助你选择最适合你的任务的泛型集合。
| 我要…… | 泛型集合选项 | 非泛型集合选项 | 线程安全或不可变集合选项 | 
|---|---|---|---|
| 将项存储为键/值对以通过键进行快速查找 | Dictionary<TKey,TValue> | Hashtable (根据键的哈希代码组织的键/值对的集合。) | ConcurrentDictionary<TKey,TValue> ReadOnlyDictionary<TKey,TValue> ImmutableDictionary<TKey,TValue> | 
| 按索引访问项 | List<T> | Array ArrayList | ImmutableList<T> ImmutableArray | 
| 使用项先进先出 (FIFO) | Queue<T> | Queue | ConcurrentQueue<T> ImmutableQueue<T> | 
| 使用数据后进先出 (LIFO) | Stack<T> | Stack | ConcurrentStack<T> ImmutableStack<T> | 
| 按顺序访问项 | LinkedList<T> | 无建议 | 无建议 | 
| 删除集合中的项或向集合添加项时接收通知。 (实现 INotifyPropertyChanged 和 INotifyCollectionChanged) | ObservableCollection<T> | 无建议 | 无建议 | 
| 已排序的集合 | SortedList<TKey,TValue> | SortedList | ImmutableSortedDictionary<TKey,TValue> ImmutableSortedSet<T> | 
| 数学函数的一个集 | HashSet<T> SortedSet<T> | 无建议 | ImmutableHashSet<T> ImmutableSortedSet<T> | 
HashSet<T> 类是用于包含唯一元素的无序集合。SortedSet<T> 类提供在执行插入、删除和搜索操作之后让数据一直按排序顺序排列的自平衡树。
抽象基类 KeyedCollection<TKey,TItem> 的行为类似列表和字典。
ConcurrentStack、ConcurrentQueue和ConcurrentBag类型内部是使用链表实现的。因此,其内存利用不如非并发的Stack和Queue高效。但是它们适用于并发访问,因为链表更容易实现无锁算法或者少锁的算法。
 ConcurrentBag<T> 表示对象的线程安全的无序集合,与集不同,包支持重复项。ConcurrentBag<T> 可以接受 null 作为引用类型的有效值。
一个ConcurrentBag<T>对象上的每一个线程都有自己的私有链表。线程在调用Add方法时会将元素添加到自己的私有链表中,因此不会出现竞争。当我们枚举集合中的元素时,其枚举器会遍历每一个线程的私有链表,依次返回
在调用Take时,ConcurrentBag<T>首先会查询当前线程的私有列表,如果列表中至少有一个元素存在[插图],那么该操作就可以在不引入竞争的情况下完成。但是,如果私有列表是空的,则必须从其他线程的私有列表中“窃取”一个元素,而这种操作可能造成竞争。
因此,准确地说,Take方法将返回调用线程在集合中最近添加的元素。如果该线程上已经没有任何元素,它会返回其他线程(随机挑选)最近添加的元素。
集合的算法复杂性
不可变的集合类型通常性能较低,但却提供了不可变性,这通常是一种非常有效的优点。
| 可变 | 分期 | 最坏情况 | 不可变 | 复杂性 | 
|---|---|---|---|---|
| Stack<T>.Push | O(1) | O( n) | ImmutableStack<T>.Push | O(1) | 
| Queue<T>.Enqueue | O(1) | O( n) | ImmutableQueue<T>.Enqueue | O(1) | 
| List<T>.Add | O(1) | O( n) | ImmutableList<T>.Add | O(log n) | 
| List<T>.Item[Int32] | O(1) | O(1) | ImmutableList<T>.Item[Int32] | O(log n) | 
| List<T>.Enumerator | O( n) | O( n) | ImmutableList<T>.Enumerator | O( n) | 
| HashSet<T>.Add, lookup | O(1) | O( n) | ImmutableHashSet<T>.Add | O(log n) | 
| SortedSet<T>.Add | O(log n) | O( n) | ImmutableSortedSet<T>.Add | O(log n) | 
| Dictionary<T>.Add | O(1) | O( n) | ImmutableDictionary<T>.Add | O(log n) | 
| Dictionary<T>lookup | O(1) | O(1) - 或者从严格意义上说,O( n) | ImmutableDictionary<T>lookup | O(log n) | 
| SortedDictionary<T>.Add | O(log n) | O( nlogn) | ImmutableSortedDictionary<T>.Add | O(log n) | 
由于其索引器的 O(log n) 时间,ImmutableList<T> 在 for 循环内的效果较差。 使用 foreach 循环枚举 ImmutableList<T> 很有效,因为 ImmutableList<T> 使用二进制树来存储其数据,而不是像 List<T> 那样使用简单数组。 数组可以非常快速地编入索引,但必须向下遍历二进制树,直到找到具有所需索引的节点。
此外,SortedSet<T> 与 ImmutableSortedSet<T> 的复杂性相同。 这是因为它们都使用了二进制树。 当然,显著的差异在于 ImmutableSortedSet<T> 使用不可变的二进制树。 由于 ImmutableSortedSet<T> 还提供了一个允许变化的 System.Collections.Immutable.ImmutableSortedSet<T>.Builder 类,因此可以同时实现不可变性和保障性能。
 SortedDictionary<TKey,TValue> 是一个有 O(log n) 插入和检索操作的已排序字典,这使它有效代替了 SortedList<TKey,TValue>。
检查是否相等
诸如 Contains、 IndexOf、 LastIndexOf和 Remove 的方法将相等比较器用于集合元素。 如果集合是泛型的,则按照以下原则比较项是否相等:
- 
如果类型 T 实现 IEquatable<T> 泛型接口,则相等比较器是该接口的 Equals 方法。 
- 
如果类型 T 未实现 IEquatable<T>,则使用 Object.Equals 。 
此外,字典集合的某些构造函数重载接受 IEqualityComparer<T> 实现,用于比较键是否相等。 
确定排序顺序
对于比较对象,有 default comparer 和 explicit comparer的概念。
默认比较器依赖至少一个正在被比较的对象来实现 IComparable 接口。 在用作列表集合的值或字典集合的键的所有类上实现 IComparable 不失为一个好办法。 对泛型集合而言,等同性比较是根据以下内容确定的:
- 
如果类型 T 实现 System.IComparable<T> 泛型接口,则默认比较器是该接口的 IComparable<T>.CompareTo(T) 方法。 
- 
如果类型 T 实现非泛型 System.IComparable 接口,则默认比较器是该接口的 IComparable.CompareTo(Object) 方法。 
- 
如果类型 T 未实现任何接口,则没有默认比较器,必须显式提供一个比较器或比较委托。 
为了提供显式比较,某些方法接受 IComparer 实现作为参数。 例如, List<T>.Sort 方法接受 System.Collections.Generic.IComparer<T> 实现。
SortedList<TKey,TValue> 类与 SortedDictionary<TKey,TValue> 类之间的一些区别。
| SortedList<TKey,TValue> 泛型类 | SortedDictionary<TKey,TValue> 泛型类 | 
|---|---|
| 返回键和值的属性是有索引的,从而允许高效的索引检索。 | 无索引的检索。 | 
| 检索属于 O(log n) 操作。 | 检索属于 O(log n) 操作。 | 
| 插入和删除通常属于 O( n) 操作;不过,对于已按排序顺序排列的数据,插入属于 O(logn) 操作,这样每个元素都可以添加到列表的末尾。 (这假设不需要调整大小。) | 插入和删除属于 O(log n) 操作。 | 
| 比 SortedDictionary<TKey,TValue> 使用更少的内存。 | 比 SortedList 非泛型类和 SortedList<TKey,TValue> 泛型类使用更多内存。 | 
对于必须可通过多个线程并发访问的已排序列表或字典,可以向派生自 ConcurrentDictionary<TKey,TValue> 的类添加排序逻辑
线程安全集合
并发集合类型使用轻量同步机制,如 SpinLock、SpinWait、SemaphoreSlim 和 CountdownEvent,这些机制是 .NET Framework 4 中的新增功能。 这些同步类型通常在将线程真正置于等待状态之前,会在短时间内使用忙旋转。 预计等待时间非常短时,旋转比等待所消耗的计算资源少得多,因为后者涉及资源消耗量大的内核转换。 对于使用旋转的集合类,这种效率意味着多个线程能够以非常快的速率添加和删除项。
BlockingCollection<T> 是一个线程安全集合类,可提供实现制造者-使用者模式。多个线程或任务可同时向集合添加项,如果集合达到其指定最大容量,则制造线程将发生阻塞,直到移除集合中的某个项。 多个使用者可以同时移除项,如果集合变空,则使用线程将发生阻塞,直到制造者添加某个项。 制造线程可调用 CompleteAdding 来指示不再添加项。 使用者将监视 IsCompleted 属性以了解集合何时为空且不再添加项。
创建 BlockingCollection<T> 时,不仅可以指定上限容量,而且可以指定要使用的集合类型。 例如,可为先进先出 (FIFO) 行为指定 ConcurrentQueue<T>,也可为后进先出 (LIFO) 行为指定 ConcurrentStack<T>。 可使用实现 IProducerConsumerCollection<T> 接口的任何集合类。 BlockingCollection<T> 的默认集合类型为 ConcurrentQueue<T>。
如果对 BlockingCollection<T>.GetConsumingEnumerable 进行了并发调用,这些调用会争用。 无法在一个枚举中观察到在另一个枚举中使用的项目。按照此方式枚举集合会在没有项可用或集合为空时阻止使用者线程,直到CompleteAdding被调用才返回
若要枚举集合而不对其进行修改,只需使用 foreach (For Each) 即可,无需使用 GetConsumingEnumerable 方法。 但是,务必要了解此类枚举表示的是某个精确时间点的集合快照。 如果其他线程在你执行循环的同时添加或删除项,则循环可能不会表示集合的实际状态。
在使用者需要同时取出多个集合中的项的情况下,可以创建 BlockingCollection<T> 的数组并使用静态方法,如 TakeFromAny 和 AddToAny 方法,这两个方法可以在该数组的任意集合中添加或取出项。 如果一个集合发生阻塞,此方法会立即尝试其他集合,直到找到能够执行该操作的集合。
//Generate some source data.
      BlockingCollection<int>[] sourceArrays = new BlockingCollection<int>[5];
      for(int i = 0; i < sourceArrays.Length; i++)
          sourceArrays[i] = new BlockingCollection<int>(500);
      Parallel.For(0, sourceArrays.Length * 500, (j) =>
                          {
                              int k = BlockingCollection<int>.TryAddToAny(sourceArrays, j);
                              if(k >=0)
                                  Console.WriteLine("added {0} to source data", j);
                          });
      foreach (var arr in sourceArrays)
          arr.CompleteAdding();
ConcurrentDictionary<TKey,TValue> 专为多线程方案而设计。 无需在代码中使用锁定即可在集合中添加或移除项。 但始终可能出现以下情况:一个线程检索一个值,而另一线程通过为同一键赋予新值来立即更新集合。
此外,尽管 ConcurrentDictionary<TKey,TValue> 的所有方法都是线程安全的,但并非所有方法都是原子的,尤其是 GetOrAdd 和 AddOrUpdate。 为避免未知代码阻止所有线程,传递给这些方法的用户委托将在词典的内部锁之外调用。 因此,可能发生以下事件序列:
- 
threadA 调用 GetOrAdd,未找到项,通过调用 valueFactory委托创建要添加的新项。
- 
threadB 并发调用 GetOrAdd,其 valueFactory委托受到调用,并且它在 threadA 之前到达内部锁,并将其新键值对添加到词典中 。
- 
threadA 的用户委托完成,此线程到达锁位置,但现在发现已有项存在。 
- 
threadA 执行“Get”,返回之前由 threadB 添加的数据 。 
因此,无法保证 GetOrAdd 返回的数据与线程的 valueFactory 创建的数据相同。 调用 AddOrUpdate 时可能发生相似的事件序列。
异常Collection was modified; enumeration operation may not execute 原因:遍历时集合被修改
.NET Core 3.0 came with C# 8.0. Since then, we have been able to modify a Dictionary<TKey,TValue> during enumeration (foreach) via .Remove and .Clear only.
Microsoft.Extensions.ObjectPool 命名空间下已存在 Microsoft.Extensions.ObjectPool.ObjectPool<T> 类型。 在需要某个类的多个实例并且创建或销毁该类的成本很高的情况下,对象池可以改进应用程序性能。
 
                    
                     
                    
                 
                    
                
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号