泛型类型
在 .NET 中,开发人员随时会使用泛型,有时隐式使用,有时显式使用。 在 .NET 中使用 LINQ 时,你是否曾经注意到,使用的正是 IEnumerable<T>? 或者,你是否曾经看到过有关使用实体框架来与数据库通信的“泛型存储库”在线示例,其中的大多数方法返回 IQueryable<T>? 你可能很想知道,这些示例中的 T 是什么意思,为什么要使用它?
泛型在 .NET Framework 2.0 中首次引入,它本质上是一个“代码模板”,可让开发者定义类型安全数据结构,无需处理实际数据类型。 例如,List<T> 是一个可以声明的泛型集合,可与 List<int>、List<string> 或 List<Person> 等任何类型结合使用。
为方便理解泛型的作用,让我们看看添加泛型之前和之后的一个特定类:ArrayList。 在 .NET Framework 1.0 中,ArrayList 元素属于 Object 类型。 添加到集合的任何元素都会以静默方式转换为 Object。 从列表读取元素时,会发生相同的情况。 此过程称为装箱和取消装箱,它会影响性能。 但除了性能之外,在编译时无法确定列表中的数据的类型,这会形成一些脆弱的代码。 泛型解决了此问题,它可以定义每个列表实例将要包含的数据类型。 例如,只能将整数添加到 List<int>,只能将人员添加到 List<Person>。
泛型还可以在运行时使用。 运行时知道你要使用的数据结构类型,并可以更高效地将数据结构存储在内存中。
下面的示例是一个小程序,演示了在运行时如何有效地了解数据结构类型:
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; namespace GenericsExample { class Program { static void Main(string[] args) { //generic list List<int> ListGeneric = new List<int> { 5, 9, 1, 4 }; //non-generic list ArrayList ListNonGeneric = new ArrayList { 5, 9, 1, 4 }; // timer for generic list sort Stopwatch s = Stopwatch.StartNew(); ListGeneric.Sort(); s.Stop(); Console.WriteLine($"Generic Sort: {ListGeneric} \n Time taken: {s.Elapsed.TotalMilliseconds}ms"); //timer for non-generic list sort Stopwatch s2 = Stopwatch.StartNew(); ListNonGeneric.Sort(); s2.Stop(); Console.WriteLine($"Non-Generic Sort: {ListNonGeneric} \n Time taken: {s2.Elapsed.TotalMilliseconds}ms"); Console.ReadLine(); } } }
此程序将生成类似下面的输出:
Generic Sort: System.Collections.Generic.List`1[System.Int32] Time taken: 0.0034ms Non-Generic Sort: System.Collections.ArrayList Time taken: 0.2592ms
在此处可以看到的第一个优点是,泛型列表的排序比非泛型列表要快得多。 还可以看到,泛型列表的类型是不同的 ([System.Int32]),而非泛型列表的类型已通用化。 由于运行时知道泛型 List<int> 的类型是 Int32,因此可以将列表元素存储在内存中的基础整数数组内;而非泛型 ArrayList 必须将每个列表元素强制转换为对象。 如本示例中所示,多余的强制转换会占用时间,降低列表排序的速度。
运行时知道泛型类型的另一个优点是可以改善调试体验。 在 C# 中调试泛型时,可以知道数据结构中每个元素的类型。 如果不使用泛型,则无从知道每个元素的类型是什么。
.NET 中的泛型集合
System.Collections.Generic
许多泛型集合类型均为非泛型类型的直接模拟。 Dictionary<TKey,TValue> 是 Hashtable 的泛型版本;它使用枚举的泛型结构 KeyValuePair<TKey,TValue> 而不是 DictionaryEntry。
List<T> 是 ArrayList 的泛型版本。 存在响应非泛型版本的泛型 Queue<T> 和 Stack<T> 类。
存在 SortedList<TKey,TValue> 的泛型和非泛型版本。 这两个版本均为字典和列表的混合。 SortedDictionary<TKey,TValue> 泛型类是一个纯字典,并且没有任何非泛型对应项。
LinkedList<T> 泛型类是真正的链接列表。 它没有任何非泛型对应项。
System.Collections.ObjectModel
Collection<T> 泛型类提供用于派生自己的泛型集合类型的基类。 ReadOnlyCollection<T> 类提供了任何从实现 IList<T> 泛型接口的类型生成只读集合的简便方法。 KeyedCollection<TKey,TItem> 泛型类提供了存储包含其自己的键的对象的方法。
其他泛型类型
Nullable<T> 泛型结构允许使用值类型,如同它们可分配 null。 这在处理数据库查询时很有用,其中字段包含可能丢失的值类型。 泛型类型参数可为任意值类型。
在 C# 和 Visual Basic 中,无需显式使用 Nullable<T>,因为语言具有可以为 null 类型的语法。 请参阅可为 null 的值类型(C# 引用)和可为 null 的值类型 (Visual Basic)。
用于操作数组和列表的泛型委托
Action<T> 泛型委托表示对指定类型的元素执行某些操作的方法。 你可以创建一种对元素执行所需操作的方法,创建 Action<T> 委托的实例来表示该方法,然后将该数组和委托传递给 Array.ForEach 静态泛型方法。 数组的每个元素都可以调用该方法。
List<T> 泛型类还提供了 ForEach 方法,该方法使用 Action<T> 委托。 此方法不属泛型方法。
备注: Array.ForEach 方法必须是静态(在 Visual Basic 中为 Shared)和泛型的,因为 Array 不是泛型类型;你可以对 Array.ForEach 指定一种类型以继续运行的唯一原因是该方法有自己的类型参数列表。 与之相反,非泛型 List<T>.ForEach 方法属于泛型类 List<T>,因此它仅使用其类的类型参数。 该类为强类型,因此此方法可以作为实例方法。
Predicate<T> 泛型委托表示的是用于确定特定元素是否满足你定义的标准的方法。 你可以将其用于 Array 的以下静态泛型方法来搜索一个或一组元素:Exists、Find、FindAll、FindIndex、FindLast、FindLastIndex 和TrueForAll。
Predicate<T> 也适用于 List<T> 泛型类相应的非泛型实例方法。
Comparison<T> 泛型委托让你能为不具有本机排序顺序的数组或列表元素提供顺序排序或重写本机排序顺序。 创建执行比较的方法,创建一个 Comparison<T> 委托的实例,以表示你的方法,然后将该数组和委托传递给 Array.Sort<T>(T[], Comparison<T>) 静态泛型方法。 List<T> 泛型类提供相应的实例方法重载,List<T>.Sort(Comparison<T>)。
Converter<TInput,TOutput>泛型委托让你能定义两个类型之间的转换,将一个类型的数组转换到另一个类型的数组,或者将一个类型的列表转换到另一个类型的列表。 创建将现有列表元素转换为新类型的方法,创建一个委托实例来表示该方法,并使用 Array.ConvertAll 泛型静态方法,从原始数组生成新类型数组;或使用 List<T>.ConvertAll<TOutput>(Converter<T,TOutput>) 泛型实例方法,从原始列表生成新类型列表。
链接委托
使用这些委托的许多方法返回数组或列表,然后传递到另一种方法。 例如,如果你想要选择某些数组元素,将这些元素转换为新类型,并将其保存在新的数组中,则可以将 FindAll 泛型方法返回的数组传递到 ConvertAll 泛型方法。 如果新的元素类型缺少自然排序顺序,你可以将 ConvertAll 泛型方法返回的数组传递到 Sort<T>(T[], Comparison<T>) 泛型方法。
泛型接口
泛型接口提供与非泛型接口对应的类型安全接口,用于实现排序比较、相等比较以及泛型集合类型所共享的功能。
备注
从 .NET Framework 4 开始,多个泛型接口的类型参数标记为协变或逆变,这为分配和使用实现这些接口的类型提供了更好的灵活性。 请参阅 协变和逆变。
相等比较和排序比较
在 System 命名空间中,System.IComparable<T> 和 System.IEquatable<T> 泛型接口与它们对应的非泛型接口一样,各自定义了用于排序比较和相等比较的方法。 类型通过实现这些接口来提供执行这些比较的能力。
在 System.Collections.Generic 命名空间中,IComparer<T> 和 IEqualityComparer<T> 泛型接口为没有实现 System.IComparable<T> 或 System.IEquatable<T> 泛型接口的类型提供一种定义排序比较和相等比较的方式,并为实现了上述泛型接口的类型提供了重新定义这些关系的方式。 这些接口由许多泛型集合类的方法和构造函数使用。 例如,可以将泛型 IComparer<T> 对象传递至 SortedDictionary<TKey,TValue> 类的构造函数,以便为没有实现泛型 System.IComparable<T> 的类型指定排列顺序。 存在 Array.Sort 泛型静态方法与通过泛型 IComparer<T> 实现对数组和列表进行排序的 List<T>.Sort 实例方法的重载。
Comparer<T> 和 EqualityComparer<T> 泛型类为 IComparer<T> 和 IEqualityComparer<T> 泛型接口的实现提供基类,同时还通过其各自的 Comparer<T>.Default 和 EqualityComparer<T>.Default 属性提供默认排序比较和相等比较。
集合功能
ICollection<T> 泛型接口是泛型集合类型的基本接口。 它提供添加、删除、复制和枚举元素的基本功能。 ICollection<T> 继承自泛型 IEnumerable<T> 和非泛型 IEnumerable。
IList<T> 泛型接口使用索引检索的方法扩展 ICollection<T> 泛型接口。
IDictionary<TKey,TValue> 泛型接口使用键控检索的方法扩展 ICollection<T> 泛型接口。 .NET Framework 基类库中的泛型字典类型还实现非泛型 IDictionary 接口。
IEnumerable<T> 泛型接口提供泛型枚举器结构。 泛型枚举器实现的 IEnumerator<T> 泛型接口继承自非泛型 IEnumerator 接口;MoveNext 和 Reset 成员(不依赖于类型参数 T)仅出现在非泛型接口中。 这意味着非泛型接口的任何使用者还可以使用泛型接口。
常用的集合类型
集合类型是数据集合(如哈希表、队列、堆栈、包、字典和列表)的常见变体。
集合基于 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 查询还可提高性能。
何时使用泛型集合
使用泛型集合可获得类型安全的自动化优点而无需从基集合类型派生和实现特定类型的成员。 当集合元素为值类型时,泛型集合类型也通常优于对应的非泛型集合类型(比从非泛型基集合类型派生的类型好),因为使用泛型时不必对元素进行装箱。
对于面向 .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> 泛型类对应有着相应的相同名称的非泛型类。
其他类型
几种泛型集合类型没有对应的非泛型集合类型。 它们包括以下类型:
-
LinkedList<T> 是一个通用的链接列表,该列表提供 O(1) 插入和删除操作。
-
SortedDictionary<TKey,TValue> 是一个有 O(log
n) 插入和检索操作的已排序字典,这使它有效代替了 SortedList<TKey,TValue>。 -
KeyedCollection<TKey,TItem> 是列表和字典的结合,它提供了一种方法来存储包含自己的键的对象。
-
BlockingCollection<T> 通过限制和阻止功能实现集合类。
-
ConcurrentBag<T> 能快速插入和移除未排序元素。
不可变生成器
如果需要在应用中使用不可变性功能,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
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)”。
其他功能
一些泛型类型具有非泛型集合类型中找不到的功能。 比如与非泛型 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> 类让你指定自己的相等比较器。
集合内的比较和排序
System.Collections 类在管理集合所涉及的几乎所有进程中执行比较,无论是搜索待删除的元素或返回键值对的值。
集合通常使用相等比较器和/或排序比较器。 有两个构造用于比较。
检查是否相等
诸如 Contains、 IndexOf、 LastIndexOf和 Remove 的方法将相等比较器用于集合元素。 如果集合是泛型的,则按照以下原则比较项是否相等:
-
如果类型 T 实现 IEquatable<T> 泛型接口,则相等比较器是该接口的 Equals 方法。
-
如果类型 T 未实现 IEquatable<T>,则使用 Object.Equals 。
此外,字典集合的某些构造函数重载接受 IEqualityComparer<T> 实现,用于比较键是否相等。 有关示例,请参见 Dictionary<TKey,TValue> 构造函数。
确定排序顺序
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。 有关详细信息,请参阅 “在集合中执行不区分区域性的字符串操作” 和 “在数组中执行不区分区域性的字符串操作”。
等同性和排序示例
以下代码展示了 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> 接口。
哈希表和字典集合类型
System.Collections.Hashtable 类以及 System.Collections.Generic.Dictionary<TKey,TValue> 和 System.Collections.Concurrent.ConcurrentDictionary<TKey,TValue> 泛型类实现 System.Collections.IDictionary 接口。 Dictionary<TKey,TValue> 泛型类还实现 IDictionary<TKey,TValue> 泛型接口。 因此,这些集合中的每个元素都是一个键值对。
Hashtable 由包含集合元素的存储桶组成。 存储桶是 Hashtable 中元素的虚拟子组,与在大多数集合中进行搜索和检索相比,其搜索和检索更加容易和快速。 每个存储桶都与一个哈希代码相关联,该哈希代码通过哈希函数生成并基于元素的键。
泛型 HashSet<T> 类是用于包含唯一元素的无序集合。
哈希函数是一种算法,返回基于键的数值哈希代码。 该键是所存储对象的某个属性的值。 哈希函数必须始终返回同一个键的同一哈希代码。 哈希函数有可能为两个不同的键生成相同的哈希代码,但从哈希表中检索元素时,为每个唯一的键生成唯一哈希代码的哈希函数具有更好的性能。
在 Hashtable 中用作元素的每个对象必须能够通过使用 GetHashCode 方法的实现为自身生成哈希代码。 但是,还可以为 Hashtable 中的所有元素指定哈希函数,方法是使用接受 IHashCodeProvider 实现作为其参数之一的 Hashtable 构造函数。
当将对象添加到 Hashtable时,其存储在与哈希代码相关联的存储桶中,此哈希代码匹配该对象的哈希代码。 当在 Hashtable 中对一个值进行搜索时,则为该值生成哈希代码,并搜索与该哈希代码相关联的存储桶。
例如,用于字符串的哈希函数可能采用字符串中每个字符的 ASCII 代码,并将它们加总以生成哈希代码。 字符串“picnic”的哈希代码可能与字符串“basket”的哈希代码不同;因此,字符串“picnic”和“basket”可能在不同的存储桶中。 与此相反,“stressed”和“desserts”可能具有相同的哈希代码,并且位于同一个存储桶中。
Dictionary<TKey,TValue> 和 ConcurrentDictionary<TKey,TValue> 类具有与 Hashtable 类相同的功能。 特定类型(不包括 Object)的 Dictionary<TKey,TValue> 与 Hashtable 相比可为值类型提供更好的性能。 这是因为 Hashtable 的元素属于 Object 类型;因此,装箱和取消装箱通常发生在存储或检索值类型时。 可能有多个线程同时访问该集合时,应使用 ConcurrentDictionary<TKey,TValue> 类。
线程安全集合
.NET Framework 4 引入了 System.Collections.Concurrent 命名空间,其中包含多个线程安全且可缩放的集合类。 多个线程可以安全高效地从这些集合添加或删除项,而无需在用户代码中进行其他同步。 编写新代码时,只要将多个线程同时写入到集合时,就使用并发集合类。 如果仅从共享集合进行读取,则可使用 System.Collections.Generic 命名空间中的类。 建议不要使用 1.0 集合类,除非需要定位 .NET Framework 1.1 或更低版本运行时。
.NET Framework 1.0 和 2.0 集合中的线程同步
.NET Framework 1.0 中引入的集合位于 System.Collections 命名空间中。 这些集合(包括常用的 ArrayList 和 Hashtable)通过 Synchronized 属性(此属性围绕集合返回线程安全的包装器)提供一些线程安全性。 该包装器通过对每个添加或删除操作锁定整个集合进行工作。 因此,每个尝试访问集合的线程必须等待,直到轮到它获取锁定。 这不可缩放,并且可能导致大型集合的性能显著下降。 此外,这一设计并不能完全防止争用情况的出现。 有关详细信息,请参阅泛型集合中的同步。
.NET Framework 2.0 中引入的集合类位于 System.Collections.Generic 命名空间中。 它们包括 List<T>、Dictionary<TKey,TValue> 等。 与 .NET Framework 1.0 类相比,这些类提升了类型安全性和性能。 不过,.NET Framework 2.0 集合类不提供任何线程同步;多线程同时添加或删除项时,用户代码必须提供所有同步。
建议使用 .NET Framework 4 中的并发集合类,因为它们不仅能够提供 .NET Framework 2.0 集合类的类型安全性,而且能够比 .NET Framework 1.0 集合更高效完整地提供线程安全性。
细粒度锁定和无锁机制
某些并发集合类型使用轻量同步机制,如 SpinLock、SpinWait、SemaphoreSlim 和 CountdownEvent,这些机制是 .NET Framework 4 中的新增功能。 这些同步类型通常在将线程真正置于等待状态之前,会在短时间内使用忙旋转。 预计等待时间非常短时,旋转比等待所消耗的计算资源少得多,因为后者涉及资源消耗量大的内核转换。 对于使用旋转的集合类,这种效率意味着多个线程能够以非常快的速率添加和删除项。 有关旋转与锁定的详细信息,请参阅 SpinLock 和 SpinWait。
ConcurrentQueue<T> 和 ConcurrentStack<T> 类完全不使用锁定。 相反,它们依赖于 Interlocked 操作来实现线程安全性。
备注
由于并发集合类支持 ICollection,因此该类可提供针对 IsSynchronized 和 SyncRoot 属性的实现,即使这些属性不相关。 IsSynchronized 始终返回 false,而 SyncRoot 始终为 null(在 Visual Basic 中是 Nothing)。
下表列出了 System.Collections.Concurrent 命名空间中的集合类型。
| 类型 | 描述 |
|---|---|
| BlockingCollection<T> | 为实现 IProducerConsumerCollection<T> 的所有类型提供限制和阻止功能。 有关详细信息,请参阅 BlockingCollection 概述。 |
| ConcurrentDictionary<TKey,TValue> | 键值对字典的线程安全实现。 |
| ConcurrentQueue<T> | FIFO(先进先出)队列的线程安全实现。 |
| ConcurrentStack<T> | LIFO(后进先出)堆栈的线程安全实现。 |
| ConcurrentBag<T> | 无序元素集合的线程安全实现。 |
| IProducerConsumerCollection<T> | 类型必须实现以在 BlockingCollection 中使用的接口。 |
相关主题
| Title | 描述 |
|---|---|
| BlockingCollection 概述 | 描述 BlockingCollection<T> 类型提供的功能。 |
| 如何:在 ConcurrentDictionary 中添加和移除项 | 描述如何从 ConcurrentDictionary<TKey,TValue> 添加和删除元素 |
| 如何:在 BlockingCollection 中逐个添加和取出项 | 描述如何在不使用只读枚举器的情况下,从阻止的集合添加和检索项。 |
| 如何:向集合添加限制和阻塞功能 | 描述如何将任一集合类用作 IProducerConsumerCollection<T> 集合的基础存储机制。 |
| 如何:使用 ForEach 移除 BlockingCollection 中的项 | 介绍了如何使用 foreach(在 Visual Basic 中是 For Each)在锁定集合中删除所有项。 |
| 如何:在管道中使用阻塞集合的数组 | 描述如何同时使用多个阻塞集合来实现一个管道。 |
| 如何:使用 ConcurrentBag 创建目标池 | 演示如何使用并发包在可重用对象(而不是继续创建新对象)的情况下改进性能。 |

浙公网安备 33010602011771号