学习 C# HashSet:特性与用法 – wiki基地


深入理解 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> 在特定场景下具有显著优势:

  1. 高效的成员资格测试 (Membership Testing): 判断一个元素是否已存在于集合中是 HashSet<T> 的强项。得益于哈希表,Contains 方法的平均时间复杂度是 O(1),远优于 List<T> 的 O(n)。
  2. 快速去重 (Fast Deduplication): 如果你有一个包含重复元素的集合(如 List<T>),可以使用 HashSet<T> 轻松快速地去除重复项。只需将所有元素添加到 HashSet<T> 中,然后可以将其转换回列表或其他集合类型。
  3. 高效的集合运算 (Efficient Set Operations): HashSet<T> 提供了专门的方法来执行并集、交集、差集、对称差集以及子集/超集判断等集合运算,这些操作通常比使用其他集合类型手动实现要高效得多。
  4. 空间效率 (相对而言): 虽然哈希表可能需要一些额外的空间来管理桶(buckets),但对于存储大量需要快速查找且唯一的元素时,其空间开销是合理的。

典型应用场景:

  • 判断元素是否存在: 快速查找某个元素是否在一个大型集合中。
  • 从列表中去除重复项: 将一个 List<T> 转换为 HashSet<T> 可以快速得到一个去重后的集合。
  • 执行复杂的集合逻辑: 需要计算两个或多个集合的并集、交集等。
  • 实现查找表 (Lookup Table) 且只需要存储键: 如果你只需要知道某些项是否存在,而不需要关联值(像 Dictionary<TKey, TValue> 那样),HashSet<T> 是一个更简洁的选择。
  • 记录已处理的项: 在处理数据流时,使用 HashSet<T> 记录已经处理过的项,避免重复处理。

3. HashSet 的创建与初始化

创建 HashSet<T> 有多种方式,以适应不同的初始化需求。

3.1. 创建一个空的 HashSet

这是最常见的创建方式,创建一个没有任何元素的集合。

“`csharp
// 创建一个存储字符串的空 HashSet
HashSet names = new HashSet();

// 创建一个存储整数的空 HashSet
HashSet numbers = new 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 duplicateNumbers = new List { 1, 2, 2, 3, 4, 4, 5, 1 };

// 从列表中创建一个 HashSet,会自动去除重复项
HashSet uniqueNumbers = new HashSet(duplicateNumbers);

// 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 = new HashSet(StringComparer.OrdinalIgnoreCase);

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 fruits = new 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 colors = new HashSet { “Red”, “Green”, “Blue” };

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 primeNumbers = new HashSet { 2, 3, 5, 7, 11 };

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 letters = new HashSet { ‘a’, ‘b’, ‘c’ };

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 vehicles = new HashSet { “Car”, “Bike”, “Truck” };
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 set1 = new HashSet { 1, 2, 3 };
HashSet set2 = new HashSet { 3, 1, 2 }; // 顺序不同
List list1 = new List { 1, 2, 3 };
List list2 = new List { 1, 2, 2, 3 }; // 包含重复

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 setA = new HashSet { 1, 2, 3 };
HashSet setB = new HashSet { 3, 4, 5 };

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 setA = new HashSet { 1, 2, 3, 4 };
HashSet setB = new HashSet { 3, 4, 5, 6 };

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 setA = new HashSet { 1, 2, 3, 4 };
HashSet setB = new HashSet { 3, 4, 5, 6 };

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 setA = new HashSet { 1, 2, 3, 4 };
HashSet setB = new HashSet { 3, 4, 5, 6 };

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 setSmall = new HashSet { 1, 2 };
HashSet setLarge = new HashSet { 1, 2, 3, 4 };
HashSet setEqual = new HashSet { 2, 1 }; // 包含相同元素
HashSet setOther = new HashSet { 3, 4 };

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)的索引上。

  1. 添加元素:
    • 计算元素的哈希码 (GetHashCode() 方法)。
    • 使用哈希码计算对应的存储桶索引。
    • 在目标存储桶中,检查是否已存在与待添加元素“相等”的元素 (Equals() 方法)。
    • 如果不存在相等的元素,则将元素添加到该存储桶中。
    • 如果存在相等的元素,则添加失败。
  2. 查找元素 (Contains):
    • 计算待查找元素的哈希码。
    • 计算对应的存储桶索引。
    • 在目标存储桶中,遍历其中的元素,使用 Equals() 方法与待查找元素进行比较。
    • 如果找到相等的元素,则表示元素存在。
    • 如果遍历完整个存储桶都没有找到相等的元素,则表示元素不存在。
  3. 移除元素 (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 等),它们已经正确实现了 EqualsGetHashCode。但如果你存储的是自定义类的实例,而你没有重写 EqualsGetHashCode,那么默认使用的是 object 类的实现,它基于对象引用进行比较。这意味着两个内容完全相同但内存地址不同的对象将被视为不相等,导致它们可以同时存在于 HashSet<T> 中,这通常不是期望的行为。

示例:未重写 Equals/GetHashCode 的问题

“`csharp
public class MyClass
{
public int ID { get; set; }
public string Name { get; set; }
}

// … 在主程序中 …
HashSet mySet = new 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 mySetCorrect = new 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
``
正确实现
EqualsGetHashCode是确保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 numbers = new HashSet { 1, 5, 10, 15, 20 };

// 使用 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 moreNumbers = new List { 10, 20, 30 };
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) 时间复杂度。然而,正确理解并实现自定义类型的 EqualsGetHashCode 方法对于充分利用 HashSet<T> 的性能至关重要。通过与 List<T>Dictionary<TKey, TValue> 等其他集合类型的对比,我们可以更清晰地认识到 HashSet<T> 的优势和适用范围。在实际开发中,合理地选择和使用 HashSet<T> 能够显著提高代码的效率和简洁性。


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部