深入理解 C# HashSet:特性、用法与性能考量
在 C# 的集合类型大家族中,System.Collections.Generic.HashSet<T>
是一个强大而独特的成员。它不同于列表(List<T>
)或数组,不存储重复元素,并且元素的顺序是无关紧要的。对于需要快速进行元素查找、去重或执行集合运算(如并集、交集)的场景,HashSet<T>
是一个理想的选择。本文将深入探讨 HashSet<T>
的核心特性、常见用法、底层原理以及与其他集合类型的对比,帮助读者全面掌握这一重要的数据结构。
1. 什么是 HashSet?核心概念解析
HashSet<T>
是 .NET Framework 和 .NET 的标准库中提供的一种无序、不包含重复元素的集合。它实现了 System.Collections.Generic.ISet<T>
、System.Collections.Generic.ICollection<T>
和 System.Collections.Generic.IEnumerable<T>
等接口。
其核心概念源于数学中的“集合”(Set)。在数学集合中,每个元素都是唯一的,且元素的顺序不重要。HashSet<T>
完美地模拟了这一概念。
关键特性概览:
- 唯一性 (Uniqueness): 集合中不允许存在两个相同的元素。当你尝试添加一个已经存在的元素时,添加操作将被忽略,并且
Add
方法会返回false
。 - 无序性 (No Order):
HashSet<T>
不保证元素的任何特定顺序。元素存储的位置取决于其哈希码,并且在集合的大小或内容发生变化时,内部顺序可能会改变。你不能通过索引访问元素。 - 基于哈希表 (Hash Table Based):
HashSet<T>
在内部使用哈希表来存储和管理元素。这使得查找、添加和删除操作在平均情况下具有非常高的效率(接近常数时间,O(1))。 - 动态大小 (Dynamic Size): 它可以根据需要自动调整内部存储容量。
2. 为何选择 HashSet?优势与应用场景
与其他集合类型相比,HashSet<T>
在特定场景下具有显著优势:
- 高效的成员资格测试 (Membership Testing): 判断一个元素是否已存在于集合中是
HashSet<T>
的强项。得益于哈希表,Contains
方法的平均时间复杂度是 O(1),远优于List<T>
的 O(n)。 - 快速去重 (Fast Deduplication): 如果你有一个包含重复元素的集合(如
List<T>
),可以使用HashSet<T>
轻松快速地去除重复项。只需将所有元素添加到HashSet<T>
中,然后可以将其转换回列表或其他集合类型。 - 高效的集合运算 (Efficient Set Operations):
HashSet<T>
提供了专门的方法来执行并集、交集、差集、对称差集以及子集/超集判断等集合运算,这些操作通常比使用其他集合类型手动实现要高效得多。 - 空间效率 (相对而言): 虽然哈希表可能需要一些额外的空间来管理桶(buckets),但对于存储大量需要快速查找且唯一的元素时,其空间开销是合理的。
典型应用场景:
- 判断元素是否存在: 快速查找某个元素是否在一个大型集合中。
- 从列表中去除重复项: 将一个
List<T>
转换为HashSet<T>
可以快速得到一个去重后的集合。 - 执行复杂的集合逻辑: 需要计算两个或多个集合的并集、交集等。
- 实现查找表 (Lookup Table) 且只需要存储键: 如果你只需要知道某些项是否存在,而不需要关联值(像
Dictionary<TKey, TValue>
那样),HashSet<T>
是一个更简洁的选择。 - 记录已处理的项: 在处理数据流时,使用
HashSet<T>
记录已经处理过的项,避免重复处理。
3. HashSet 的创建与初始化
创建 HashSet<T>
有多种方式,以适应不同的初始化需求。
3.1. 创建一个空的 HashSet
这是最常见的创建方式,创建一个没有任何元素的集合。
“`csharp
// 创建一个存储字符串的空 HashSet
HashSet
// 创建一个存储整数的空 HashSet
HashSet
“`
3.2. 使用初始容量创建
你可以为 HashSet<T>
指定一个初始容量。虽然 HashSet<T>
会根据需要自动调整大小,但如果你事先知道大致的元素数量,指定一个合适的初始容量可以减少后续扩容(rehashing)的开销,从而提高性能。
csharp
// 创建一个初始容量为 100 的 HashSet
HashSet<string> largeSet = new HashSet<string>(100);
3.3. 从另一个集合初始化
你可以使用任何实现了 IEnumerable<T>
接口的集合来初始化 HashSet<T>
。这是一种快速去重和初始化 HashSet
的方法。
“`csharp
// 假设有一个包含重复元素的列表
List
// 从列表中创建一个 HashSet,会自动去除重复项
HashSet
// uniqueNumbers 现在包含 { 1, 2, 3, 4, 5 } (顺序不确定)
Console.WriteLine($”Unique numbers count: {uniqueNumbers.Count}”); // 输出 5
foreach (int num in uniqueNumbers)
{
Console.Write($”{num} “); // 输出 1 2 3 4 5 或其他顺序
}
Console.WriteLine();
“`
3.4. 使用自定义相等比较器初始化
对于自定义类型或者需要特殊相等比较规则(例如字符串的大小写不敏感比较)的情况,你可以提供一个实现了 IEqualityComparer<T>
接口的实例。
“`csharp
// 创建一个字符串 HashSet,使用不区分大小写的比较器
HashSet
caseInsensitiveSet.Add(“Apple”);
caseInsensitiveSet.Add(“apple”); // 这不会被添加,因为 “apple” 与 “Apple” 被视为相等
Console.WriteLine($”Count: {caseInsensitiveSet.Count}”); // 输出 1
Console.WriteLine($”Contains ‘Apple’: {caseInsensitiveSet.Contains(“Apple”)}”); // 输出 True
Console.WriteLine($”Contains ‘apple’: {caseInsensitiveSet.Contains(“apple”)}”); // 输出 True
“`
4. HashSet 的基本操作
了解如何添加、删除、查找和遍历元素是使用 HashSet<T>
的基础。
4.1. 添加元素 (Add)
使用 Add(T item)
方法向集合中添加一个元素。如果元素成功添加(即之前不存在),方法返回 true
;如果元素已经存在,方法返回 false
,且集合内容不变。
“`csharp
HashSet
bool addedApple = fruits.Add(“Apple”); // 添加成功,addedApple = true
bool addedBanana = fruits.Add(“Banana”); // 添加成功,addedBanana = true
bool addedAppleAgain = fruits.Add(“Apple”); // 添加失败,因为 “Apple” 已存在,addedAppleAgain = false
Console.WriteLine($”Added Apple: {addedApple}”);
Console.WriteLine($”Added Banana: {addedBanana}”);
Console.WriteLine($”Added Apple Again: {addedAppleAgain}”);
Console.WriteLine($”Current count: {fruits.Count}”); // 输出 2
“`
4.2. 移除元素 (Remove)
使用 Remove(T item)
方法从集合中移除一个元素。如果元素成功移除(即元素存在于集合中),方法返回 true
;如果元素不存在于集合中,方法返回 false
。
“`csharp
HashSet
bool removedGreen = colors.Remove(“Green”); // 移除成功,removedGreen = true
bool removedYellow = colors.Remove(“Yellow”); // 移除失败,因为 “Yellow” 不存在,removedYellow = false
Console.WriteLine($”Removed Green: {removedGreen}”);
Console.WriteLine($”Removed Yellow: {removedYellow}”);
Console.WriteLine($”Current count: {colors.Count}”); // 输出 2 (Red, Blue)
“`
4.3. 检查元素是否存在 (Contains)
使用 Contains(T item)
方法检查集合中是否包含某个元素。这是 HashSet<T>
最常用的操作之一,并且非常高效。
“`csharp
HashSet
Console.WriteLine($”Contains 5: {primeNumbers.Contains(5)}”); // 输出 True
Console.WriteLine($”Contains 4: {primeNumbers.Contains(4)}”); // 输出 False
“`
4.4. 获取元素数量 (Count)
使用 Count
属性获取集合中当前元素的数量。
csharp
HashSet<string> planets = new HashSet<string> { "Earth", "Mars", "Jupiter" };
Console.WriteLine($"Number of planets: {planets.Count}"); // 输出 3
4.5. 遍历集合 (Iteration)
HashSet<T>
实现了 IEnumerable<T>
接口,因此可以使用 foreach
循环遍历集合中的所有元素。但请记住,遍历的顺序是不确定的,并且可能与元素的添加顺序无关。
“`csharp
HashSet
Console.WriteLine(“Letters in the set:”);
foreach (char letter in letters)
{
Console.Write($”{letter} “); // 输出顺序可能不同,如 a b c 或 c b a 等
}
Console.WriteLine();
``
HashSet
**重要提示:** 在遍历时,不应该修改集合(添加或移除元素),否则会抛出
InvalidOperationException。如果需要在遍历时修改,可以先将元素复制到另一个集合(如
List) 中,然后遍历新集合并修改原
HashSet
4.6. 清空集合 (Clear)
使用 Clear()
方法移除集合中的所有元素。
“`csharp
HashSet
Console.WriteLine($”Count before clear: {vehicles.Count}”); // 输出 3
vehicles.Clear();
Console.WriteLine($”Count after clear: {vehicles.Count}”); // 输出 0
“`
4.7. 检查集合是否相等 (SetEquals)
使用 SetEquals(IEnumerable<T> other)
方法检查当前集合与另一个集合(可以是任何 IEnumerable<T>
)是否包含完全相同的元素。元素的顺序和数量在比较中是无关紧要的,只要两个集合包含的唯一元素完全一致即可。
“`csharp
HashSet
HashSet
List
List
Console.WriteLine($”set1 equals set2: {set1.SetEquals(set2)}”); // 输出 True
Console.WriteLine($”set1 equals list1: {set1.SetEquals(list1)}”); // 输出 True
Console.WriteLine($”set1 equals list2: {set1.SetEquals(list2)}”); // 输出 True (因为list2的唯一元素是1,2,3)
“`
5. HashSet 的高级操作:集合运算
HashSet<T>
提供了强大的集合运算方法,可以直接对两个集合进行并集、交集等操作。这些方法修改的是当前 HashSet
对象。
5.1. 并集 (UnionWith)
UnionWith(IEnumerable<T> other)
方法修改当前 HashSet<T>
,使其包含自身所有元素以及另一个集合 other
中的所有元素。
“`csharp
HashSet
HashSet
setA.UnionWith(setB); // setA 现在包含 { 1, 2, 3, 4, 5 } (顺序不确定)
Console.Write(“Union: “);
foreach (int num in setA) { Console.Write($”{num} “); }
Console.WriteLine();
“`
5.2. 交集 (IntersectWith)
IntersectWith(IEnumerable<T> other)
方法修改当前 HashSet<T>
,使其只包含自身与另一个集合 other
共有的元素。
“`csharp
HashSet
HashSet
setA.IntersectWith(setB); // setA 现在包含 { 3, 4 } (顺序不确定)
Console.Write(“Intersection: “);
foreach (int num in setA) { Console.Write($”{num} “); }
Console.WriteLine();
“`
5.3. 差集 (ExceptWith)
ExceptWith(IEnumerable<T> other)
方法修改当前 HashSet<T>
,移除所有也在另一个集合 other
中的元素。
“`csharp
HashSet
HashSet
setA.ExceptWith(setB); // setA 现在包含 { 1, 2 } (顺序不确定)
Console.Write(“Difference (A – B): “);
foreach (int num in setA) { Console.Write($”{num} “); }
Console.WriteLine();
“`
5.4. 对称差集 (SymmetricExceptWith)
SymmetricExceptWith(IEnumerable<T> other)
方法修改当前 HashSet<T>
,使其只包含那些只存在于当前集合或只存在于另一个集合 other
中的元素(即不在两个集合的交集中的元素)。
“`csharp
HashSet
HashSet
setA.SymmetricExceptWith(setB); // setA 现在包含 { 1, 2, 5, 6 } (顺序不确定)
Console.Write(“Symmetric Difference: “);
foreach (int num in setA) { Console.Write($”{num} “); }
Console.WriteLine();
“`
5.5. 子集/超集判断
IsSubsetOf(IEnumerable<T> other)
: 判断当前集合是否是另一个集合other
的子集(即当前集合的所有元素都存在于other
中)。IsProperSubsetOf(IEnumerable<T> other)
: 判断当前集合是否是另一个集合other
的真子集(即当前集合是other
的子集,并且other
包含至少一个当前集合没有的元素)。IsSupersetOf(IEnumerable<T> other)
: 判断当前集合是否是另一个集合other
的超集(即other
的所有元素都存在于当前集合中)。IsProperSupersetOf(IEnumerable<T> other)
: 判断当前集合是否是另一个集合other
的真超集(即当前集合是other
的超集,并且当前集合包含至少一个other
没有的元素)。
“`csharp
HashSet
HashSet
HashSet
HashSet
Console.WriteLine($”setSmall is subset of setLarge: {setSmall.IsSubsetOf(setLarge)}”); // True
Console.WriteLine($”setSmall is proper subset of setLarge: {setSmall.IsProperSubsetOf(setLarge)}”); // True
Console.WriteLine($”setEqual is subset of setLarge: {setEqual.IsSubsetOf(setLarge)}”); // True
Console.WriteLine($”setEqual is proper subset of setLarge: {setEqual.IsProperSubsetOf(setLarge)}”); // False (因为setEqual不是setLarge的真子集)
Console.WriteLine($”setLarge is superset of setSmall: {setLarge.IsSupersetOf(setSmall)}”); // True
Console.WriteLine($”setLarge is proper superset of setSmall: {setLarge.IsProperSupersetOf(setSmall)}”); // True
Console.WriteLine($”setLarge is superset of setEqual: {setLarge.IsSupersetOf(setEqual)}”); // True
Console.WriteLine($”setLarge is proper superset of setEqual: {setLarge.IsProperSupersetOf(setEqual)}”); // True (注意:setLarge包含setEqual所有元素,且setLarge有setEqual没有的元素3,4)
Console.WriteLine($”setSmall is subset of setOther: {setSmall.IsSubsetOf(setOther)}”); // False
Console.WriteLine($”setSmall is superset of setOther: {setSmall.IsSupersetOf(setOther)}”); // False
“`
6. HashSet 的底层原理与性能考量
HashSet<T>
的高性能主要归功于其底层实现的哈希表。
6.1. 哈希表的工作原理简述
哈希表通过一个哈希函数 (Hash Function) 将键(在 HashSet<T>
中,元素本身就是“键”)映射到存储桶(buckets)的索引上。
- 添加元素:
- 计算元素的哈希码 (
GetHashCode()
方法)。 - 使用哈希码计算对应的存储桶索引。
- 在目标存储桶中,检查是否已存在与待添加元素“相等”的元素 (
Equals()
方法)。 - 如果不存在相等的元素,则将元素添加到该存储桶中。
- 如果存在相等的元素,则添加失败。
- 计算元素的哈希码 (
- 查找元素 (
Contains
):- 计算待查找元素的哈希码。
- 计算对应的存储桶索引。
- 在目标存储桶中,遍历其中的元素,使用
Equals()
方法与待查找元素进行比较。 - 如果找到相等的元素,则表示元素存在。
- 如果遍历完整个存储桶都没有找到相等的元素,则表示元素不存在。
- 移除元素 (
Remove
):- 计算待移除元素的哈希码。
- 计算对应的存储桶索引。
- 在目标存储桶中,遍历元素并使用
Equals()
方法查找待移除元素。 - 如果找到,则移除该元素并返回
true
。 - 如果未找到,则返回
false
。
6.2. 性能分析
- 平均情况: 如果哈希函数能够将元素均匀地分布到不同的存储桶中,那么每个存储桶中的元素数量会很少。在这种理想情况下,添加、查找和删除操作的时间复杂度是 O(1)(常数时间),因为只需要计算一次哈希码,然后在一个很小的集合(存储桶)中进行查找/比较。
- 最坏情况: 如果哈希函数设计得不好,或者输入数据非常特殊,导致大量元素被映射到同一个存储桶(发生哈希冲突),那么该存储桶可能会变得非常大。在这种情况下,添加、查找和删除操作需要遍历整个存储桶,时间复杂度退化为 O(n),其中 n 是集合中的元素总数。然而,.NET Framework/Core 的哈希函数通常设计得很好,并且
HashSet<T>
内部有机制(如链表或树结构存储桶中的元素,以及在负载因子过高时进行扩容 (rehashing))来缓解哈希冲突的影响,使其在实际应用中很少出现 O(n) 的情况。 - 空间复杂度: O(n),需要与元素数量成正比的内存空间,加上哈希表结构本身的一些开销。
6.3. 对自定义类型的要求:Equals 和 GetHashCode
当 HashSet<T>
存储的是自定义类型 T
时,为了保证正确性和性能,类型 T
必须正确地实现了 Equals(object obj)
方法和 GetHashCode()
方法。
GetHashCode()
方法用于计算元素的哈希码,决定元素应该放在哪个存储桶。Equals()
方法用于在同一个存储桶内比较两个元素是否相等。
重要的契约:
- 如果两个对象根据
Equals()
方法判断是相等的,那么它们的GetHashCode()
方法必须返回相同的值。 - 如果两个对象的
GetHashCode()
返回相同的值,它们不一定相等(这是哈希冲突的正常现象),但它们可能相等,需要通过Equals()
方法进一步判断。 GetHashCode()
的实现应该尽可能地将不同的对象分散到不同的哈希码,以减少冲突。GetHashCode()
应该快速计算且在对象的哈希相关字段不变时始终返回相同的值。
如果你存储的是基本类型(如 int
, string
, DateTime
等),它们已经正确实现了 Equals
和 GetHashCode
。但如果你存储的是自定义类的实例,而你没有重写 Equals
和 GetHashCode
,那么默认使用的是 object
类的实现,它基于对象引用进行比较。这意味着两个内容完全相同但内存地址不同的对象将被视为不相等,导致它们可以同时存在于 HashSet<T>
中,这通常不是期望的行为。
示例:未重写 Equals/GetHashCode 的问题
“`csharp
public class MyClass
{
public int ID { get; set; }
public string Name { get; set; }
}
// … 在主程序中 …
HashSet
MyClass obj1 = new MyClass { ID = 1, Name = “Test” };
MyClass obj2 = new MyClass { ID = 1, Name = “Test” }; // 内容与 obj1 相同,但不是同一个实例
mySet.Add(obj1);
mySet.Add(obj2); // obj2 会被成功添加,因为默认的 Equals/GetHashCode 基于引用
Console.WriteLine($”Count: {mySet.Count}”); // 输出 2 (可能不是你期望的)
“`
示例:重写 Equals/GetHashCode 解决问题
“`csharp
public class MyClassCorrect
{
public int ID { get; set; }
public string Name { get; set; }
// 重写 Equals
public override bool Equals(object obj)
{
if (obj == null || GetType() != obj.GetType())
{
return false;
}
MyClassCorrect other = (MyClassCorrect)obj;
// 根据 ID 和 Name 判断相等
return ID == other.ID && Name == other.Name;
}
// 重写 GetHashCode
public override int GetHashCode()
{
// 组合 ID 和 Name 的哈希码。可以利用 System.HashCode.Combine (.NET Core/.NET 5+)
return HashCode.Combine(ID, Name);
// 或者手动组合 (旧版本兼容)
// int hashCode = 17;
// hashCode = hashCode * 23 + ID.GetHashCode();
// hashCode = hashCode * 23 + (Name != null ? Name.GetHashCode() : 0);
// return hashCode;
}
}
// … 在主程序中 …
HashSet
MyClassCorrect obj3 = new MyClassCorrect { ID = 1, Name = “Test” };
MyClassCorrect obj4 = new MyClassCorrect { ID = 1, Name = “Test” };
mySetCorrect.Add(obj3);
mySetCorrect.Add(obj4); // obj4 不会被添加,因为 Equals/GetHashCode 认为它与 obj3 相等
Console.WriteLine($”Count: {mySetCorrect.Count}”); // 输出 1
``
Equals
正确实现和
GetHashCode是确保
HashSet(以及
Dictionary
7. HashSet 与其他集合类型的对比
理解 HashSet<T>
的最佳方式之一是将其与 C# 中其他常用的集合类型进行对比。
特性 | HashSet<T> |
List<T> |
Dictionary<TKey, TValue> |
SortedSet<T> |
Array |
---|---|---|---|---|---|
元素唯一性 | 强制唯一 | 允许重复 | 键强制唯一,值允许重复 | 强制唯一 | 允许重复 |
元素顺序 | 无序 (哈希码决定,内部可能变) | 维护插入顺序 | 无序 (哈希码决定,内部可能变) | 元素按排序规则排序 | 维护定义顺序或赋值顺序 |
查找元素 | Contains(item) : 平均 O(1) |
Contains(item) : O(n) |
ContainsKey(key) : 平均 O(1) |
Contains(item) : O(log n) |
线性搜索 O(n) |
添加元素 | Add(item) : 平均 O(1) |
Add(item) : 平均 O(1) (末尾) |
Add(key, value) : 平均 O(1) |
Add(item) : O(log n) |
固定大小,无法直接“添加” |
删除元素 | Remove(item) : 平均 O(1) |
Remove(item) : O(n) (查找) + O(n) (移动元素) |
Remove(key) : 平均 O(1) |
Remove(item) : O(log n) |
固定大小,无法直接“删除” |
通过索引访问 | 不支持 | 支持 ([index] ) |
不支持 (通过键访问) | 不支持 | 支持 ([index] ) |
内存占用 | O(n) + 哈希表开销 | O(n) | O(n) + 哈希表开销 | O(n) + 平衡树开销 | O(n) |
实现接口 | ISet<T> , ICollection<T> , IEnumerable<T> |
IList<T> , ICollection<T> , IEnumerable<T> |
IDictionary<TKey, TValue> , ICollection<KeyValuePair<TKey, TValue>> , IEnumerable<KeyValuePair<TKey, TValue>> |
ISet<T> , ICollection<T> , IEnumerable<T> |
IList , ICollection , IEnumerable (非泛型); 泛型数组直接实现 IList<T> , ICollection<T> , IEnumerable<T> |
主要用途 | 去重、快速成员测试、集合运算 | 有序列表、随机访问 | 键值对存储、通过键查找 | 有序唯一元素集、范围查询 | 固定大小有序集合、快速索引访问 |
何时选择 HashSet<T>
而不是其他集合?
- 当你需要存储一组唯一的元素,并且对元素的顺序没有要求时。
- 当你需要频繁地检查某个元素是否已经存在于集合中(成员资格测试)。
- 当你需要执行集合运算(并集、交集、差集等)。
- 当你需要从另一个集合(如
List<T>
)中快速去除重复项时。
何时不选择 HashSet<T>
?
- 当你需要存储允许重复的元素时,使用
List<T>
或数组。 - 当你需要保持元素的添加顺序或以特定顺序(如排序)访问元素时,使用
List<T>
、LinkedList<T>
或SortedSet<T>
。 - 当你需要通过元素的索引进行访问时,使用
List<T>
或数组。 - 当你需要存储键值对时,使用
Dictionary<TKey, TValue>
或SortedDictionary<TKey, TValue>
。
8. 线程安全
HashSet<T>
不是线程安全的。这意味着如果多个线程可能同时对同一个 HashSet<T>
实例进行读写操作,你需要自己实现同步机制(例如使用 lock
关键字)。
如果需要在多线程环境中安全地访问集合,可以考虑使用 System.Collections.Concurrent
命名空间下的并发集合类型,例如 ConcurrentDictionary<TKey, TValue>
(虽然没有直接的 ConcurrentHashSet
,但 ConcurrentDictionary
可以模拟很多 HashSet
的行为,例如通过 TryAdd
方法实现唯一性,通过 ContainsKey
实现快速查找)。
9. LINQ 与 HashSet
HashSet<T>
实现了 IEnumerable<T>
接口,这意味着你可以无缝地使用 LINQ(Language Integrated Query)对其进行查询操作。
“`csharp
HashSet
// 使用 LINQ 查找所有大于 10 的元素
var largeNumbers = numbers.Where(n => n > 10).ToList(); // ToList() 将结果转换回列表
Console.WriteLine(“Numbers greater than 10:”);
foreach (int num in largeNumbers)
{
Console.Write($”{num} “); // 输出 15 20 (顺序可能不确定)
}
Console.WriteLine();
// 使用 LINQ 的 Distinct() 方法对 HashSet 调用是多余的,因为它本身就保证唯一
// var uniqueNumbers = numbers.Distinct(); // 结果仍然是 HashSet 中的唯一元素
// LINQ 也提供了一些集合操作方法,但它们通常会创建一个新的集合,
// 而 HashSet 自身的方法 (UnionWith, IntersectWith 等) 是修改当前集合。
List
var combined = numbers.Union(moreNumbers); // LINQ Union,返回新的 IEnumerable
Console.WriteLine(“Combined (LINQ Union):”);
foreach (int num in combined)
{
Console.Write($”{num} “); // 输出 1 5 10 15 20 30 (顺序不确定)
}
Console.WriteLine();
“`
10. 总结
HashSet<T>
是 C# 中一个高效且实用的集合类型,特别适用于需要存储唯一元素、进行快速成员资格测试以及执行集合运算的场景。其底层基于哈希表的实现保证了大多数操作在平均情况下的 O(1) 时间复杂度。然而,正确理解并实现自定义类型的 Equals
和 GetHashCode
方法对于充分利用 HashSet<T>
的性能至关重要。通过与 List<T>
、Dictionary<TKey, TValue>
等其他集合类型的对比,我们可以更清晰地认识到 HashSet<T>
的优势和适用范围。在实际开发中,合理地选择和使用 HashSet<T>
能够显著提高代码的效率和简洁性。