写作原因:数据类型很重要,集合类型更重要,这次从微软官网扒下,记录下集合的概念和选型方案等知识,如有不对的地方,请多多指教。
一、集合和数据结构
类似的数据在作为集合而存储和操作时通常可以得到更高效地处理。 可以使用 System.Array 类或 System.Collections、System.Collections.Generic、System.Collections.Concurrent 和 System.Collections.Immutable 命名空间中的类来添加、删除和修改集合中的任一单个元素或某个范围的元素。
有两种主要的集合类型:泛型集合和非泛型集合。 泛型集合在编译时是类型安全的。 因此,泛型集合通常能提供更好的性能。
二、常用集合功能
所有集合都提供用于在集合中添加、删除或查找项的方法。 此外,所有直接或间接实现 ICollection 接口或 ICollection<T> 接口的集合均共享这些功能:
2.1 可枚举集合
.NET 集合实现 System.Collections.IEnumerable 或 System.Collections.Generic.IEnumerable<T>,以启用要循环访问的集合。 可将枚举器看作集合中可指向任何元素的可移动指针。 foreach, in 语句和 For Each...Next 语句使用 GetEnumerator 方法公开的枚举器并隐藏操作枚举器的复杂性。 此外,任何实现 System.Collections.Generic.IEnumerable<T> 的集合均被认为是可查询类型,并可使用 LINQ 对其进行查询。 LINQ 查询提供数据访问的一个通用模式。 它们通常比标准 foreach
循环更简洁、更具可读性,并提供筛选、排序和分组功能。 LINQ 查询还可提高性能。 有关详细信息,请参阅 LINQ to Objects (C#)、LINQ to Objects (Visual Basic)、并行 LINQ (PLINQ)、LINQ 查询 (C#) 简介和基本查询操作 (Visual Basic)。
2.2 可将集合内容复制到数组
可使用 CopyTo 方法将所有集合复制到数组中;但新数组中的元素顺序是以枚举器返回元素的顺序为依据。 得到的数组始终是一维的,下限为零。
2.3 容量和计数属性
集合的容量是它可包含的元素数。 集合的计数是它实际所含的元素数。 某些集合隐藏容量、计数或将这两者都隐藏。
达到当前容量时,大多数集合会自动扩展容量。 重新分配内存并将元素从旧集合复制到新集合。 这减少了要求使用集合的代码;但集合的性能可能会受到不利影响。 例如,对 List<T> 来说,如果 Count 比 Capacity 少,那么添加项就是一项 O(1) 操作。 如需增加容量以容纳新元素,则添加项成为 O(n
) 操作,其中 n
是 Count。 避免因多次重新分配而导致的性能较差的最佳方式是:将初始容量设置为集合的估计大小。
BitArray 是一种特殊情况;它的容量与其长度相同,而其长度与其计数相同。
2.4 下限一致
集合的下限是其第一个元素的索引。 System.Collections 命名空间中的所有索引集合的下限均为零,这表示它们从 0 开始建立索引。 Array 默认下限为零,但使用 Array.CreateInstance 创建 Array 类的实例时可定义其他下限。
2.5 同步以从多个线程进行访问(仅 System.Collections 类)
System.Collections 命名空间中的非泛型集合类型通过同步提供一些线程安全性;通常通过 SyncRoot 和 IsSynchronized 成员公开。 这些集合不是默认为线程安全的。 如需对集合进行可扩展、高效的多线程访问,请使用 System.Collections.Concurrent 命名空间中的一个类或考虑使用不可变集合。 有关详细信息,请参阅线程安全集合。
三、选择集合
一般情况下,应使用泛型集合。 下表介绍了一些常用的集合方案和可用于这些方案的集合类。 如果你是使用泛型集合的新手,此表将帮助你选择最适合你的任务的泛型集合。
需求 | 泛型集合选项 | 非泛型集合选项 | 线程安全或不可变集合选项 |
将项存储为键/值对以通过键进行快速查找 | 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> | 无建议 |
- 是否需要顺序列表(其中通常在检索元素值后就将该元素丢弃)?
在需要的情况下,如果需要先进先出 (FIFO) 行为,请考虑使用 Queue 类或 Queue<T> 泛型类。 如果需要后进先出 (LIFO) 行为,请考虑使用 Stack 类或 Stack<T> 泛型类。 若要从多个线程进行安全访问,请使用并发版本(ConcurrentQueue<T> 和 ConcurrentStack<T>)。 如果要获得不可变性,请考虑不可变版本 ImmutableQueue<T> 和 ImmutableStack<T>。
如果不需要,请考虑使用其他集合。
- 是否需要以特定顺序(如先进先出、后进先出或随机)访问元素?
Queue 类以及 Queue<T>、ConcurrentQueue<T> 和 ImmutableQueue<T> 泛型类都提供 FIFO 访问权限。 有关详细信息,请参阅何时使用线程安全集合。
Stack 类以及 Stack<T>、ConcurrentStack<T> 和 ImmutableStack<T> 泛型类都提供 LIFO 访问权限。 有关详细信息,请参阅何时使用线程安全集合。
LinkedList<T> 泛型类允许从开头到末尾或从末尾到开头的顺序访问。
- 是否需要按索引访问每个元素?
ArrayList 和 StringCollection 类以及 List<T> 泛型类按从零开始的元素索引提供对其元素的访问。 如果要获得不可变性,请考虑不可变泛型版本 ImmutableArray<T> 和 ImmutableList<T>。
Hashtable、SortedList、ListDictionary 和 StringDictionary 类以及 Dictionary<TKey,TValue> 和 SortedDictionary<TKey,TValue> 泛型类按元素的键提供对其元素的访问。 此外,还有几个相应类型的不可变版本:ImmutableHashSet<T>、ImmutableDictionary<TKey,TValue>、ImmutableSortedSet<T> 和 ImmutableSortedDictionary<TKey,TValue>。
NameObjectCollectionBase 和 NameValueCollection 类以及 KeyedCollection<TKey,TItem> 和 SortedList<TKey,TValue> 泛型类按从零开始的元素索引或元素的键提供对其元素的访问。
- 是否每个元素都包含一个值、一个键和一个值的组合或一个键和多个值的组合?
一个值:使用任何基于 IList 接口或 IList<T> 泛型接口的集合。 要获得不可变选项,请考虑 IImmutableList<T> 泛型接口。
一个键和一个值:使用任何基于 IDictionary 接口或 IDictionary<TKey,TValue> 泛型接口的集合。 要获得不可变选项,请考虑 IImmutableSet<T> 或 IImmutableDictionary<TKey,TValue> 泛型接口。
带有嵌入键的值:使用 KeyedCollection<TKey,TItem> 泛型类。
一个键和多个值:使用 NameValueCollection 类。
- 是否需要以与输入方式不同的方式对元素进行排序?
Hashtable 类按其哈希代码对其元素进行排序。
SortedList 类以及 SortedList<TKey,TValue> 和 SortedDictionary<TKey,TValue> 泛型类按键对元素进行排序。 排序顺序的依据为,实现 SortedList 类的 IComparer 接口和实现 SortedList<TKey,TValue> 和 SortedDictionary<TKey,TValue> 泛型类的 IComparer<T> 泛型接口。 在这两种泛型类型中,虽然 SortedDictionary<TKey,TValue> 的性能优于 SortedList<TKey,TValue>,但 SortedList<TKey,TValue> 占用的内存更少。
ArrayList 提供了一种 Sort 方法,此方法采用 IComparer 实现作为参数。 其泛型对应项(List<T> 泛型类)提供一种 Sort 方法,此方法采用 IComparer<T> 泛型接口的实现作为参数。
- 是否需要快速搜索和信息检索?
对于小集合(10 项或更少),ListDictionary 速度比 Hashtable 快。 Dictionary<TKey,TValue> 泛型类提供比 SortedDictionary<TKey,TValue> 泛型类更快的查找。 多线程的实现为 ConcurrentDictionary<TKey,TValue>。 ConcurrentBag<T> 为无序数据提供快速的多线程插入。 有关这两种多线程类型的详细信息,请参阅何时使用线程安全集合。
- 是否需要只接受字符串的集合?
StringCollection(基于 IList)和 StringDictionary(基于 IDictionary)位于 System.Collections.Specialized 命名空间。
此外,通过指定其泛型类参数的 String 类,可以使用 System.Collections.Generic 命名空间中的任何泛型集合类作为强类型字符串集合。 例如,可以将变量声明为采用 List<String> 或 Dictionary<String,String> 类型。
四、常用集合类型
集合类型是数据集合(如哈希表、队列、堆栈、包、字典和列表)的常见变体。
集合基于 ICollection 接口、 IList 接口、IDictionary 接口或它们对应的泛型集合。 IList 接口和 IDictionary 接口都派生自 ICollection 接口:因此,所有集合都直接或间接基于 ICollection 接口。 在基于 IList 接口(如 Array、ArrayList 或 List<T>)或直接基于 ICollection 接口(如 Queue、ConcurrentQueue<T>、Stack、ConcurrentStack<T> 或 LinkedList<T>)的集合里,每个元素都只有一个值。 在基于 IDictionary 接口(比如 Hashtable 和 SortedList 类,Dictionary<TKey,TValue> 和SortedList<TKey,TValue> 泛型类)或 ConcurrentDictionary<TKey,TValue> 类的集合中,每个元素都有一个键和一个值。 KeyedCollection<TKey,TItem> 类是唯一的,因为它是值中嵌键的值的列表,因此,它的行为类似列表和字典。
泛型集合都是强类型的最佳解决方案。 但,如果你的语言不支持泛型,那么 System.Collections 命名空间包含基集合,如 CollectionBase、ReadOnlyCollectionBase 和 DictionaryBase,这些集合都是可扩展以创建强类型集合类的抽象基类。 需要高效的多线程集合访问时,请使用 System.Collections.Concurrent 命名空间中的泛型集合。
集合会因元素的存储方式、排序方式、执行搜索的方式以及比较方式的不同而不同。 Queue 类和 Queue<T> 泛型类提供先进先出列表,而 Stack 类和 Stack<T> 泛型类提供后进先出列表。 SortedList 类和 SortedList<TKey,TValue> 泛型类提供 Hashtable 类和 Dictionary<TKey,TValue> 泛型类的已排序版本。 Hashtable 或 Dictionary<TKey,TValue> 的元素只能通过元素的键访问,但 SortedList 或 KeyedCollection<TKey,TItem> 的元素能通过元素的键或索引访问。 所有集合中的索引都从零开始,Array 除外,它允许不从零开始的数组。
LINQ to Objects 功能让你可以通过使用 LINQ 查询来访问内存中的对象,条件是该对象类型实现 IEnumerable 或 IEnumerable<T>。 LINQ 查询提供了一种通用的数据访问模式;与标准 foreach
循环相比,它通常更加简洁;可读性更高,并且可提供筛选、排序和分组功能。 LINQ 查询还可提高性能。 有关详细信息,请参阅 “LINQ to Objects (C#)”、“LINQ to Objects (Visual Basic)” 和 “并行 LINQ (PLINQ)”。
五、集合内的比较和排序
System.Collections 类在管理集合所涉及的几乎所有进程中执行比较,无论是搜索待删除的元素或返回键值对的值。
集合通常使用相等比较器和/或排序比较器。 有两个构造用于比较。
5.1 检查是否相等
诸如 Contains
、 IndexOf、 LastIndexOf和 Remove
的方法将相等比较器用于集合元素。 如果集合是泛型的,则按照以下原则比较项是否相等:
-
如果类型 T 实现 IEquatable<T> 泛型接口,则相等比较器是该接口的 Equals 方法。
-
如果类型 T 未实现 IEquatable<T>,则使用 Object.Equals 。
此外,字典集合的某些构造函数重载接受 IEqualityComparer<T> 实现,用于比较键是否相等。 有关示例,请参见 Dictionary<TKey,TValue> 构造函数。
5.2 确定排序顺序
BinarySearch
和 Sort
等方法将排序比较器用于集合元素。 可在集合的元素间进行比较,或在元素或指定值之间进行比较。 对于比较对象,有 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> 实现。
系统当前的区域性设置可影响集合中的比较和排序。 默认情况下, Collections 类中的比较和排序是区分区域性的。 若要忽略区域性设置并因此获得一致的比较和排序结果,请使用具有接受 InvariantCulture 的成员重载的 CultureInfo。 有关详细信息,请参阅 “在集合中执行不区分区域性的字符串操作” 和 “在数组中执行不区分区域性的字符串操作”。
5.3 等同性和排序示例
以下代码展示了 IEquatable<T> 和 IComparable<T> 在简单的业务对象上的实现。 此外,如果对象被存储在列表中并已排序,那么你会发现调用 Sort() 方法会导致 Part
类型使用默认比较器,并通过使用匿名方法实现 Sort(Comparison<T>) 方法。
using System;
using System.Collections.Generic;
// Simple business object. A PartId is used to identify the
// type of part but the part name can change.
public class Part : IEquatable<Part>, IComparable<Part>
{
public string PartName { get; set; }
public int PartId { get; set; }
public override string ToString() =>
$"ID: {PartId} Name: {PartName}";
public override bool Equals(object obj) =>
(obj is Part part)
? Equals(part)
: false;
public int SortByNameAscending(string name1, string name2) =>
name1?.CompareTo(name2) ?? 1;
// Default comparer for Part type.
// A null value means that this object is greater.
public int CompareTo(Part comparePart) =>
comparePart == null ? 1 : PartId.CompareTo(comparePart.PartId);
public override int GetHashCode() => PartId;
public bool Equals(Part other) =>
other is null ? false : PartId.Equals(other.PartId);
// Should also override == and != operators.
}
public class Example
{
public static void Main()
{
// Create a list of parts.
var parts = new List<Part>
{
// Add parts to the list.
new Part { PartName = "regular seat", PartId = 1434 },
new Part { PartName = "crank arm", PartId = 1234 },
new Part { PartName = "shift lever", PartId = 1634 },
// Name intentionally left null.
new Part { PartId = 1334 },
new Part { PartName = "banana seat", PartId = 1444 },
new Part { PartName = "cassette", PartId = 1534 }
};
// Write out the parts in the list. This will call the overridden
// ToString method in the Part class.
Console.WriteLine("\nBefore sort:");
parts.ForEach(Console.WriteLine);
// Call Sort on the list. This will use the
// default comparer, which is the Compare method
// implemented on Part.
parts.Sort();
Console.WriteLine("\nAfter sort by part number:");
parts.ForEach(Console.WriteLine);
// This shows calling the Sort(Comparison<T> comparison) overload using
// a lambda expression as the Comparison<T> delegate.
// This method treats null as the lesser of two values.
parts.Sort((Part x, Part y) =>
x.PartName == null && y.PartName == null
? 0
: x.PartName == null
? -1
: y.PartName == null
? 1
: x.PartName.CompareTo(y.PartName));
Console.WriteLine("\nAfter sort by name:");
parts.ForEach(Console.WriteLine);
/*
Before sort:
ID: 1434 Name: regular seat
ID: 1234 Name: crank arm
ID: 1634 Name: shift lever
ID: 1334 Name:
ID: 1444 Name: banana seat
ID: 1534 Name: cassette
After sort by part number:
ID: 1234 Name: crank arm
ID: 1334 Name:
ID: 1434 Name: regular seat
ID: 1444 Name: banana seat
ID: 1534 Name: cassette
ID: 1634 Name: shift lever
After sort by name:
ID: 1334 Name:
ID: 1444 Name: banana seat
ID: 1534 Name: cassette
ID: 1234 Name: crank arm
ID: 1434 Name: regular seat
ID: 1634 Name: shift lever
*/
}
}
六、已排序的集合类型
System.Collections.SortedList 类、System.Collections.Generic.SortedList<TKey,TValue> 泛型类和 System.Collections.Generic.SortedDictionary<TKey,TValue> 泛型类与 Hashtable 类和 Dictionary<TKey,TValue> 泛型类的相似之处在于均实现 IDictionary 接口,不同之处在于它们让元素一直按键的排序顺序排列,并且不具备哈希表的 O(1) 插入和检索特性。 这三个类具有若干共性:
-
这三个类全都实现 System.Collections.IDictionary 接口。 两个泛型类还实现 System.Collections.Generic.IDictionary<TKey,TValue> 泛型接口。
-
每个元素都是用于枚举的键/值对。
备注
枚举时,非泛型 SortedList 类返回 DictionaryEntry 对象,尽管两个泛型类型返回 KeyValuePair<TKey,TValue> 对象。
-
元素按 System.Collections.IComparer 实现代码(对于非泛型 SortedList)或 System.Collections.Generic.IComparer<T> 实现代码(对于两个泛型类)进行排序。
-
每个类提供了返回仅包含键或仅包含值的集合的属性。
下表列出了两个已排序列表类与 SortedDictionary<TKey,TValue> 类之间的一些区别。
SortedList 非泛型类和 SortedList<TKey,TValue> 泛型类 | SortedDictionary<TKey,TValue> 泛型类 |
---|---|
返回键和值的属性是有索引的,从而允许高效的索引检索。 | 无索引的检索。 |
检索属于 O(log n ) 操作。 | 检索属于 O(log n ) 操作。 |
插入和删除通常属于 O(n ) 操作;不过,对于已按排序顺序排列的数据,插入属于 O(log n ) 操作,这样每个元素都可以添加到列表的末尾。 (这假设不需要调整大小。) | 插入和删除属于 O(log n ) 操作。 |
比 SortedDictionary<TKey,TValue> 使用更少的内存。 | 比 SortedList 非泛型类和 SortedList<TKey,TValue> 泛型类使用更多内存。 |
对于必须可通过多个线程并发访问的已排序列表或字典,可以向派生自 ConcurrentDictionary<TKey,TValue> 的类添加排序逻辑。 考虑不可变性时,以下相应的不可变类型遵循类似的排序语义:ImmutableSortedSet<T> 和 ImmutableSortedDictionary<TKey,TValue>。
备注
对于包含自己的键的值(例如,包含雇员 ID 编号的雇员记录),可以通过派生自 KeyedCollection<TKey,TItem> 泛型类,创建具有列表和字典的某些特性的键控集合。
从 .NET Framework 4 开始,SortedSet<T> 类提供在执行插入、删除和搜索操作之后让数据一直按排序顺序排列的自平衡树。 此类和 HashSet<T> 类实现 ISet<T> 接口。
七、何时使用泛型集合
使用泛型集合可获得类型安全的自动化优点而无需从基集合类型派生和实现特定类型的成员。 当集合元素为值类型时,泛型集合类型也通常优于对应的非泛型集合类型(比从非泛型基集合类型派生的类型好),因为使用泛型时不必对元素进行装箱。
对于面向 .NET Standard 1.0 或更高版本的程序,请在多个线程可能会同时向集合添加项或从集合中删除项时使用 System.Collections.Concurrent 命名空间中的泛型集合。 此外,当需要不可变性时,请考虑 System.Collections.Immutable 命名空间中的泛型集合类。
以下泛型类型对应于现有集合类型:
-
Dictionary<TKey,TValue> 和 ConcurrentDictionary<TKey,TValue> 泛型类对应 Hashtable。
-
Collection<T> 泛型类对应于 CollectionBase。 Collection<T> 可以用作基类,但是与 CollectionBase 不同,它不抽象,这大大降低了其使用难度。
-
ReadOnlyCollection<T> 泛型类对应于 ReadOnlyCollectionBase。 ReadOnlyCollection<T> 不是抽象的并且拥有可以轻松地公开现有的 List<T> 为只读集合的构造函数。
-
Queue<T>、 ConcurrentQueue<T>、 ImmutableQueue<T>、 ImmutableArray<T>、SortedList<TKey,TValue> 和 ImmutableSortedSet<T> 泛型类对应有着相应的相同名称的非泛型类。
7.1 其他类型
几种泛型集合类型没有对应的非泛型集合类型。 它们包括以下类型:
-
LinkedList<T> 是一个通用的链接列表,该列表提供 O(1) 插入和删除操作。
-
SortedDictionary<TKey,TValue> 是一个有 O(log
n
) 插入和检索操作的已排序字典,这使它有效代替了 SortedList<TKey,TValue>。 -
KeyedCollection<TKey,TItem> 是列表和字典的结合,它提供了一种方法来存储包含自己的键的对象。
-
BlockingCollection<T> 通过限制和阻止功能实现集合类。
-
ConcurrentBag<T> 能快速插入和移除未排序元素。
7.2 不可变生成器
如果需要在应用中使用不可变性功能,System.Collections.Immutable 命名空间提供了可使用的泛型集合类型。 所有不可变的集合类型都提供 Builder
类,此类在执行多个突变时可以优化性能。 Builder
类在不可变状态下批处理操作。 所有突变都完成后,调用 ToImmutable
方法来“冻结”所有节点并创建一个不可变的泛型集合,例如 ImmutableList<T>。
可以通过调用非泛型 CreateBuilder()
方法来创建 Builder
对象。 通过 Builder
实例,可以调用 ToImmutable()
。 同样,通过 Immutable*
集合中,可以调用 ToBuilder()
从泛型不可变集合创建生成器实例。 以下是各种 Builder
类型。
- ImmutableArray<T>.Builder
- ImmutableDictionary<TKey,TValue>.Builder
- ImmutableHashSet<T>.Builder
- ImmutableList<T>.Builder
- ImmutableSortedDictionary<TKey,TValue>.Builder
- ImmutableSortedSet<T>.Builder
7.3 LINQ to Objects
你可以通过 LINQ to Objects 功能使用 LINQ 查询来访问内存中的对象,但条件是该对象类型要实现 System.Collections.IEnumerable 或 System.Collections.Generic.IEnumerable<T> 接口。 LINQ 查询提供了一种通用的数据访问模式;与标准 foreach
循环相比,它通常更加简洁;可读性更高,并且可提供筛选、排序和分组功能。 LINQ 查询还可提高性能。 有关详细信息,请参阅 “LINQ to Objects (C#)”、“LINQ to Objects (Visual Basic)” 和 “并行 LINQ (PLINQ)”。
7.4 其他功能
一些泛型类型具有非泛型集合类型中找不到的功能。 比如与非泛型 List<T> 类相对的 ArrayList 类有大量接受泛型委托的方法,例如允许你指定搜索列表的方法的 Predicate<T> 委托、代表对列表中每个元素发挥作用的 Action<T> 委托和在类型间定义对话的 Converter<TInput,TOutput> 委托。
List<T> 类使你可以指定你自己的用于排序和搜索列表的 IComparer<T> 泛型接口实现。 SortedDictionary<TKey,TValue> 和 SortedList<TKey,TValue> 类也有这个功能。 另外,这些类使你可以在创建集合时指定比较器。 同样地,Dictionary<TKey,TValue> 和 KeyedCollection<TKey,TItem> 类让你指定自己的相等比较器。