C# HashSet 入门教程 – wiki基地


C# HashSet<T> 入门教程:独特、高效的集合类型

在 C# 编程中,我们经常需要处理各种数据的集合。System.Collections.Generic 命名空间提供了多种强大的集合类型,以满足不同的需求,例如 List<T> 用于有序列表,Dictionary<TKey, TValue> 用于键值对查找等等。然而,当我们的核心需求是存储一系列独一无二的元素,并且需要极速地执行添加删除查找(判断元素是否存在)操作时,HashSet<T> 便成为了一个理想的选择。

本篇教程将带您深入了解 C# 中的 HashSet<T>,包括它的基本概念、核心特性、常用操作、性能优势、如何处理自定义类型以及它在实际应用中的价值。无论您是 C# 初学者还是希望更深入理解 .NET 集合框架的开发者,都能从中获益。

1. 什么是 HashSet<T>?为什么使用它?

HashSet<T> 是 .NET Framework 和 .NET Core 提供的一种无序的包含唯一元素的集合类型。它的名字中的 “Hash” 暗示了其内部实现依赖于散列(Hashing)技术,这正是其高性能的关键所在。

核心特性总结:

  1. 元素唯一性: HashSet<T> 不允许存储重复的元素。当您尝试添加一个已经存在于集合中的元素时,添加操作会被忽略(或者说,Add 方法会返回 false),而不会抛出异常。
  2. 无序性: HashSet<T> 不保证元素的存储顺序或遍历顺序。元素在集合中的位置取决于其哈希码和内部存储结构,这可能随着元素的添加和删除而改变。您不能通过索引来访问 HashSet<T> 中的元素。
  3. 高性能操作: 基于散列原理,HashSet<T> 在平均情况下,其添加 (Add)、删除 (Remove) 和查找 (Contains) 操作的时间复杂度接近于 O(1)——常数时间。这意味着无论集合中有多少元素,这些操作的耗时大致保持不变,这对于处理大量数据时至关重要。
  4. 实现了 ISet<T> 接口: HashSet<T> 实现了 ISet<T> 接口,这使得它具备了丰富的集合操作方法,如并集、交集、差集等。

为什么选择 HashSet<T>

考虑以下常见的编程场景:

  • 您有一个包含大量用户 ID 的列表,需要快速判断某个用户 ID 是否已经在列表中。
  • 您从多个数据源获取了一系列数据项,需要将它们合并到一个集合中,并自动去除重复项。
  • 您需要执行两个集合的数学集合运算,例如找出共同存在的元素(交集)或只存在于其中一个集合中的元素(差集)。
  • 您需要跟踪一个过程中已经处理过的唯一项,以避免重复处理。

在这些场景下,如果使用 List<T>,判断元素是否存在 (Contains) 需要遍历整个列表,时间复杂度为 O(n)(n 为元素数量)。当 n 很大时,这会非常低效。而 HashSet<T> 的 O(1) 平均时间复杂度则能提供显著的性能提升。

因此,当“唯一性”和“高速查找/添加/删除”是您的主要需求时,HashSet<T> 通常是比 List<T> 或其他集合类型更好的选择。

2. HashSet<T> 的基本使用

首先,确保您在代码文件的顶部引入了 System.Collections.Generic 命名空间:

csharp
using System.Collections.Generic;

2.1 创建 HashSet<T>

创建 HashSet<T> 非常简单,与创建大多数泛型集合类似,需要指定存储的元素类型 T

创建空的 HashSet

“`csharp
// 创建一个存储整数的 HashSet
HashSet uniqueNumbers = new HashSet();

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

您也可以在创建时使用现有的集合初始化 HashSet。这将自动将现有集合中的元素添加到新的 HashSet 中,并自动处理重复项。

“`csharp
List numbersList = new List { 1, 2, 3, 2, 4, 1, 5 };
// 使用 List 初始化 HashSet,重复的元素 (1, 2) 只会被添加一次
HashSet uniqueNumbersFromList = new HashSet(numbersList);

Console.WriteLine($”Initial list had {numbersList.Count} elements.”); // 输出: Initial list had 7 elements.
Console.WriteLine($”HashSet created from list has {uniqueNumbersFromList.Count} unique elements.”); // 输出: HashSet created from list has 5 unique elements.
// uniqueNumbersFromList 现在包含 {1, 2, 3, 4, 5}
“`

2.2 添加元素 (Add)

使用 Add 方法向 HashSet 中添加元素。Add 方法返回一个布尔值:如果元素成功添加到集合中(即该元素之前不存在),则返回 true;如果元素已经存在于集合中,则返回 false,并且不会再次添加该元素。

“`csharp
HashSet fruits = new HashSet();

bool addedApple = fruits.Add(“Apple”); // 添加 “Apple”,返回 true
Console.WriteLine($”Added ‘Apple’: {addedApple}”); // 输出: Added ‘Apple’: True

bool addedBanana = fruits.Add(“Banana”); // 添加 “Banana”,返回 true
Console.WriteLine($”Added ‘Banana’: {addedBanana}”); // 输出: Added ‘Banana’: True

bool addedAppleAgain = fruits.Add(“Apple”); // 尝试再次添加 “Apple”,返回 false
Console.WriteLine($”Added ‘Apple’ again: {addedAppleAgain}”); // 输出: Added ‘Apple’ again: False

Console.WriteLine($”Current number of fruits: {fruits.Count}”); // 输出: Current number of fruits: 2
“`

2.3 检查元素是否存在 (Contains)

使用 Contains 方法可以高效地检查集合中是否包含某个特定的元素。这是 HashSet 最常用的操作之一,并且如前所述,其平均性能非常高(O(1))。

“`csharp
HashSet colors = new HashSet { “Red”, “Green”, “Blue” };

bool hasRed = colors.Contains(“Red”); // 检查是否存在 “Red”,返回 true
Console.WriteLine($”Contains ‘Red’: {hasRed}”); // 输出: Contains ‘Red’: True

bool hasYellow = colors.Contains(“Yellow”); // 检查是否存在 “Yellow”,返回 false
Console.WriteLine($”Contains ‘Yellow’: {hasYellow}”); // 输出: Contains ‘Yellow’: False
“`

2.4 删除元素 (Remove)

使用 Remove 方法从集合中删除指定的元素。Remove 方法也返回一个布尔值:如果成功找到并删除了元素,则返回 true;如果元素不存在于集合中,则返回 false

“`csharp
HashSet animals = new HashSet { “Lion”, “Tiger”, “Elephant” };

Console.WriteLine($”Before removal, count: {animals.Count}”); // 输出: Before removal, count: 3

bool removedTiger = animals.Remove(“Tiger”); // 删除 “Tiger”,返回 true
Console.WriteLine($”Removed ‘Tiger’: {removedTiger}”); // 输出: Removed ‘Tiger’: True
Console.WriteLine($”After removing Tiger, count: {animals.Count}”); // 输出: After removing Tiger, count: 2

bool removedZebra = animals.Remove(“Zebra”); // 尝试删除不存在的 “Zebra”,返回 false
Console.WriteLine($”Removed ‘Zebra’: {removedZebra}”); // 输出: Removed ‘Zebra’: False
Console.WriteLine($”After attempting to remove Zebra, count: {animals.Count}”); // 输出: After attempting to remove Zebra, count: 2
“`

2.5 遍历 HashSet

HashSet<T> 实现了 IEnumerable<T> 接口,这意味着您可以使用 foreach 循环来遍历集合中的所有元素。请记住,遍历的顺序是不确定的。

“`csharp
HashSet numbers = new HashSet { 10, 20, 30, 40 };

Console.WriteLine(“Iterating through the numbers:”);
foreach (int number in numbers)
{
Console.WriteLine(number);
}
// 输出顺序可能是 10, 20, 30, 40,但也可能是其他顺序,不要依赖它。
“`

重要提示:foreach 循环遍历 HashSet 的过程中,不要尝试使用 AddRemove 方法修改集合。这会导致 InvalidOperationException 异常。如果您需要在遍历时修改集合,通常的策略是:
1. 创建要删除或添加的元素的临时列表。
2. 遍历原始集合,将需要修改的元素添加到临时列表中。
3. 遍历临时列表,对原始集合执行添加或删除操作。

“`csharp
HashSet items = new HashSet { “A”, “B”, “C”, “D”, “E” };
List itemsToRemove = new List();

foreach (string item in items)
{
if (item.Contains(“C”) || item.Contains(“E”))
{
itemsToRemove.Add(item);
}
}

foreach (string item in itemsToRemove)
{
items.Remove(item);
}

Console.WriteLine(“\nItems after removal:”);
foreach (string item in items)
{
Console.WriteLine(item); // 输出 A, B, D (顺序不确定)
}
“`

2.6 获取元素数量 (Count)

使用 Count 属性可以获取集合中当前元素的数量。

csharp
HashSet<string> fruits = new HashSet<string> { "Apple", "Banana", "Orange" };
Console.WriteLine($"Number of fruits: {fruits.Count}"); // 输出: Number of fruits: 3

2.7 清空集合 (Clear)

使用 Clear 方法可以移除集合中的所有元素。

“`csharp
HashSet mySet = new HashSet { 1, 2, 3, 4, 5 };
Console.WriteLine($”Count before clear: {mySet.Count}”); // 输出: Count before clear: 5

mySet.Clear();
Console.WriteLine($”Count after clear: {mySet.Count}”); // 输出: Count after clear: 0
“`

3. HashSet<T> 的核心原理:唯一性与哈希

理解 HashSet 如何保证唯一性和实现高性能的关键在于理解哈希(Hashing)

当您向 HashSet 中添加一个元素时,HashSet 会执行以下步骤:

  1. 计算元素的哈希码: 它会调用元素的 GetHashCode() 方法来获取一个整数值,称为哈希码。
  2. 查找哈希码对应的存储位置(桶/Bucket): HashSet 内部使用一个数组(或类似的结构)来组织元素,每个位置对应一个或一组哈希码范围。哈希码决定了元素可能被存储在哪个“桶”中。
  3. 在桶中检查元素是否已存在: 在找到对应的桶后,HashSet 会遍历该桶中已经存在的元素,并使用元素的 Equals() 方法与要添加的元素进行比较。
  4. 添加或忽略: 如果在桶中找到了一个与要添加的元素相等的元素(Equals() 返回 true),则认为该元素已存在,Add 操作返回 false。如果遍历完桶中的元素都没有找到相等的元素,则将新元素添加到该桶中,Add 操作返回 true

唯一性的保证: HashSet 通过 Equals() 方法来判断两个元素是否“相等”。只有当要添加的元素与集合中已存在的任何元素都不相等时,它才会被添加到集合中。

高性能的原因: HashSet 的高效性来自于第一步和第二步。计算哈希码和找到对应的桶通常是非常快的操作。理想情况下,如果哈希函数能够将不同的元素均匀地分布到不同的桶中,那么每个桶中的元素数量会非常少。这样,第三步(在桶中进行 Equals 比较)的工作量就非常小,使得整个添加、查找、删除操作的平均时间复杂度接近 O(1)。

哈希冲突(Hash Collision): 不同的元素计算出相同的哈希码是可能发生的,这被称为哈希冲突。当哈希冲突发生时,多个元素会被存储在同一个桶中。这会增加第三步中 Equals 比较的次数。如果哈希函数设计得不好,导致大量元素集中在少数几个桶中,HashSet 的性能可能会退化,极端情况下甚至可能接近 O(n)。然而,.NET 内置类型的哈希函数通常经过优化,能很好地分散哈希码。

4. 处理自定义类型(Custom Types)

HashSet<T> 存储的是 .NET 的内置类型(如 int, string, double 等)时,它们已经正确地重写了 Equals()GetHashCode() 方法,所以 HashSet 的唯一性和性能可以正常工作。

但是,当您希望在 HashSet 中存储您自己定义的类(自定义对象)的实例时,您需要特别注意。默认情况下,如果没有重写 Equals()GetHashCode(),对于引用类型的对象,Equals() 比较的是引用相等性(是否指向同一个内存地址),而 GetHashCode() 返回的是基于对象内存地址的哈希码。这意味着即使两个不同的对象实例包含完全相同的数据,它们也会被 HashSet 视为两个不同的元素,因为它们的引用不同,哈希码也可能不同。

为了让 HashSet 正确地将两个具有相同内容的自定义对象视为重复项,您必须在您的类中重写(Override) Equals(object obj)GetHashCode() 方法。通常,建议同时实现 IEquatable<T> 接口,以提供类型安全的 Equals 方法。

示例:自定义 Person 类

假设我们有一个 Person 类:

“`csharp
public class Person
{
public int Id { get; set; }
public string Name { get; set; }

// 默认的 ToString 方法,方便打印
public override string ToString()
{
    return $"Id: {Id}, Name: {Name}";
}

}
“`

如果在 HashSet<Person> 中使用这个类,并且添加两个 IdName 都相同但不同实例的 Person 对象,它们会被认为是不同的:

“`csharp
HashSet personsDefault = new HashSet();

Person p1 = new Person { Id = 1, Name = “Alice” };
Person p2 = new Person { Id = 2, Name = “Bob” };
Person p3 = new Person { Id = 1, Name = “Alice” }; // 与 p1 内容相同,但不同实例

personsDefault.Add(p1); // 添加 p1
personsDefault.Add(p2); // 添加 p2
bool addedP3Default = personsDefault.Add(p3); // 尝试添加 p3

Console.WriteLine($”Added p3 (default behavior): {addedP3Default}”); // 输出: Added p3 (default behavior): True
Console.WriteLine($”Count (default behavior): {personsDefault.Count}”); // 输出: Count (default behavior): 3
// 默认情况下,p1, p2, p3 都被添加到集合中,尽管 p1 和 p3 内容相同。
“`

为了让 HashSet 正确识别内容相同的 Person 对象为重复项,我们需要重写 EqualsGetHashCode

“`csharp
public class PersonWithEquality : IEquatable
{
public int Id { get; set; }
public string Name { get; set; }

// 实现 IEquatable<T> 接口的 Equals 方法
public bool Equals(PersonWithEquality other)
{
    if (other == null) return false;
    if (ReferenceEquals(this, other)) return true; // 如果是同一个对象引用,直接相等
    // 比较内容是否相等
    return Id == other.Id && Name == other.Name;
}

// 重写 object 版本的 Equals 方法
public override bool Equals(object obj)
{
    return Equals(obj as PersonWithEquality); // 调用类型安全的 Equals 方法
}

// 重写 GetHashCode 方法
// 哈希码应该基于用于 Equals 比较的那些字段
public override int GetHashCode()
{
    // 结合 Id 和 Name 的哈希码生成一个总的哈希码
    // 可以使用 Tuple 或内置的 HashCode.Combine 方法 (.NET Core 3.0+)
    // 对于较旧的 .NET 版本,可以使用自定义组合逻辑,如异或 ^ 或乘以素数
    int hash = 17; // 选择一个素数作为起始值
    hash = hash * 23 + Id.GetHashCode(); // 结合 Id 的哈希码
    hash = hash * 23 + (Name != null ? Name.GetHashCode() : 0); // 结合 Name 的哈希码 (注意处理 null)
    return hash;

    // 或者使用 HashCode.Combine (.NET Core 3.0+ / .NET 5+)
    // return HashCode.Combine(Id, Name);
}

public override string ToString()
{
    return $"Id: {Id}, Name: {Name}";
}

}
“`

现在,使用 PersonWithEquality 类:

“`csharp
HashSet personsWithEquality = new HashSet();

PersonWithEquality p4 = new PersonWithEquality { Id = 1, Name = “Alice” };
PersonWithEquality p5 = new PersonWithEquality { Id = 2, Name = “Bob” };
PersonWithEquality p6 = new PersonWithEquality { Id = 1, Name = “Alice” }; // 与 p4 内容相同,但不同实例

personsWithEquality.Add(p4); // 添加 p4
personsWithEquality.Add(p5); // 添加 p5
bool addedP6Equality = personsWithEquality.Add(p6); // 尝试添加 p6

Console.WriteLine($”Added p6 (with equality logic): {addedP6Equality}”); // 输出: Added p6 (with equality logic): False
Console.WriteLine($”Count (with equality logic): {personsWithEquality.Count}”); // 输出: Count (with equality logic): 2
// 现在 p6 没有被添加,因为它的内容与 p4 相同,HashSet 识别为重复。
“`

重要原则:
* 如果两个对象根据 Equals 方法判断为相等,它们的 GetHashCode 方法必须返回相同的哈希码。
* 反之则不要求:两个对象可以具有相同的哈希码,但根据 Equals 方法判断为不相等(这就是哈希冲突)。
* GetHashCode 的实现应该基于那些用于 Equals 比较的字段。
* 确保用于计算哈希码的字段在对象被添加到 HashSet 之后不会改变,因为哈希码变化会导致 HashSet 无法正确查找或管理该元素。

5. ISet<T> 接口提供的集合操作

HashSet<T> 实现了 ISet<T> 接口,这为我们提供了执行数学集合运算的强大方法。这些操作是 HashSet 的另一个重要特性。

假设我们有两个 HashSet<int>

csharp
HashSet<int> setA = new HashSet<int> { 1, 2, 3, 4, 5 };
HashSet<int> setB = new HashSet<int> { 4, 5, 6, 7, 8 };

5.1 并集 (UnionWith)

UnionWith(otherSet) 修改当前 HashSet,使其包含当前集合和 otherSet 中的所有独特元素。

csharp
HashSet<int> unionSet = new HashSet<int>(setA); // 创建 setA 的副本
unionSet.UnionWith(setB);
// unionSet 现在包含 {1, 2, 3, 4, 5, 6, 7, 8}
Console.WriteLine("Union:");
foreach (int i in unionSet) Console.Write($"{i} "); // 输出: 1 2 3 4 5 6 7 8 (顺序不定)
Console.WriteLine();

5.2 交集 (IntersectWith)

IntersectWith(otherSet) 修改当前 HashSet,使其只包含同时存在于当前集合和 otherSet 中的元素。

csharp
HashSet<int> intersectSet = new HashSet<int>(setA); // 创建 setA 的副本
intersectSet.IntersectWith(setB);
// intersectSet 现在包含 {4, 5}
Console.WriteLine("Intersection:");
foreach (int i in intersectSet) Console.Write($"{i} "); // 输出: 4 5 (顺序不定)
Console.WriteLine();

5.3 差集 (ExceptWith)

ExceptWith(otherSet) 修改当前 HashSet,使其包含存在于当前集合但不存在于 otherSet 中的元素。

“`csharp
HashSet exceptSetAB = new HashSet(setA); // 创建 setA 的副本
exceptSetAB.ExceptWith(setB);
// exceptSetAB 现在包含 {1, 2, 3}
Console.WriteLine(“A Except B:”);
foreach (int i in exceptSetAB) Console.Write($”{i} “); // 输出: 1 2 3 (顺序不定)
Console.WriteLine();

HashSet exceptSetBA = new HashSet(setB); // 创建 setB 的副本
exceptSetBA.ExceptWith(setA);
// exceptSetBA 现在包含 {6, 7, 8}
Console.WriteLine(“B Except A:”);
foreach (int i in exceptSetBA) Console.Write($”{i} “); // 输出: 6 7 8 (顺序不定)
Console.WriteLine();
“`

5.4 对称差集 (SymmetricExceptWith)

SymmetricExceptWith(otherSet) 修改当前 HashSet,使其包含只存在于当前集合或 otherSet 中的元素(即排除两个集合的交集)。

csharp
HashSet<int> symmetricExceptSet = new HashSet<int>(setA); // 创建 setA 的副本
symmetricExceptSet.SymmetricExceptWith(setB);
// symmetricExceptSet 现在包含 {1, 2, 3, 6, 7, 8}
Console.WriteLine("Symmetric Except:");
foreach (int i in symmetricExceptSet) Console.Write($"{i} "); // 输出: 1 2 3 6 7 8 (顺序不定)
Console.WriteLine();

5.5 子集/超集/重叠判断

ISet<T> 还提供了一系列方法来判断两个集合之间的关系:

  • IsSubsetOf(otherSet): 如果当前集合的所有元素都存在于 otherSet 中,则返回 true
  • IsProperSubsetOf(otherSet): 如果当前集合是 otherSet 的子集,并且 otherSet 包含至少一个当前集合没有的元素(即不是相等的集合),则返回 true
  • IsSupersetOf(otherSet): 如果 otherSet 的所有元素都存在于当前集合中,则返回 true
  • IsProperSupersetOf(otherSet): 如果当前集合是 otherSet 的超集,并且当前集合包含至少一个 otherSet 没有的元素(即不是相等的集合),则返回 true
  • Overlaps(otherSet): 如果当前集合和 otherSet 至少有一个共同的元素,则返回 true
  • SetEquals(otherSet): 如果当前集合和 otherSet 包含完全相同的元素(忽略顺序),则返回 true

“`csharp
HashSet smallSet = new HashSet { 2, 3 };
HashSet largeSet = new HashSet { 1, 2, 3, 4 };
HashSet sameSet = new HashSet { 4, 3, 2, 1 }; // 元素相同,顺序不同
HashSet disjointSet = new HashSet { 5, 6 };

Console.WriteLine($”smallSet is subset of largeSet: {smallSet.IsSubsetOf(largeSet)}”); // True
Console.WriteLine($”smallSet is proper subset of largeSet: {smallSet.IsProperSubsetOf(largeSet)}”); // True
Console.WriteLine($”largeSet is superset of smallSet: {largeSet.IsSupersetOf(smallSet)}”); // True
Console.WriteLine($”largeSet is proper superset of smallSet: {largeSet.IsProperSupersetOf(smallSet)}”); // True
Console.WriteLine($”setA overlaps with setB: {setA.Overlaps(setB)}”); // True (共享 4, 5)
Console.WriteLine($”smallSet overlaps with disjointSet: {smallSet.Overlaps(disjointSet)}”); // False
Console.WriteLine($”largeSet equals sameSet: {largeSet.SetEquals(sameSet)}”); // True
“`

注意: UnionWith, IntersectWith, ExceptWith, SymmetricExceptWith 方法会修改调用它们的 HashSet 对象本身。如果您需要保留原始集合并在新的集合中存储结果,您应该先创建一个副本,然后对副本执行操作,或者创建一个新的 HashSet 并将操作结果添加进去。例如,new HashSet<T>(setA).UnionWith(setB) 就不会修改 setA。更常见的是使用 LINQ 的集合方法(如 Union, Intersect, Except),它们返回一个新的集合,不修改原集合。

6. 其他构造函数和 IEqualityComparer<T>

除了前面提到的无参构造函数和接受 IEnumerable<T> 的构造函数外,HashSet<T> 还有允许您指定自定义相等比较器的构造函数。

csharp
public HashSet(IEqualityComparer<T>? comparer);
public HashSet(IEnumerable<T> collection, IEqualityComparer<T>? comparer);

这里的 comparer 是一个实现了 IEqualityComparer<T> 接口的对象。这个接口定义了两个方法:

csharp
bool Equals(T x, T y);
int GetHashCode(T obj);

提供自定义比较器有以下好处:

  • 您可以在不修改类定义的情况下,为外部类提供自定义的相等性逻辑。
  • 您可以针对同一类在不同场景下使用不同的相等性规则。

例如,如果您有一个 Product 类,默认比较器可能基于产品 ID,但您可能需要一个 HashSet 来存储产品,只关心产品名称是否相同(忽略 ID)。这时就可以创建一个实现 IEqualityComparer<Product> 的类,并在创建 HashSet 时传入它。

示例:自定义字符串比较器(忽略大小写)

虽然 string 本身提供了忽略大小写的比较选项,但这只是一个简单的例子来演示 IEqualityComparer 的用法。

“`csharp
public class CaseInsensitiveStringComparer : IEqualityComparer
{
public bool Equals(string x, string y)
{
return string.Equals(x, y, StringComparison.OrdinalIgnoreCase);
}

public int GetHashCode(string obj)
{
    // 使用忽略大小写的哈希码生成方式
    return obj?.ToUpperInvariant().GetHashCode() ?? 0;
}

}

// 使用自定义比较器创建 HashSet
HashSet caseInsensitiveSet = new HashSet(new CaseInsensitiveStringComparer());

caseInsensitiveSet.Add(“Apple”);
caseInsensitiveSet.Add(“banana”); // 小写
bool addedAppleAgain = caseInsensitiveSet.Add(“APPLE”); // 大写,但内容相同(忽略大小写)

Console.WriteLine($”Added ‘APPLE’ (case-insensitive): {addedAppleAgain}”); // 输出: Added ‘APPLE’ (case-insensitive): False
Console.WriteLine($”Count (case-insensitive): {caseInsensitiveSet.Count}”); // 输出: Count (case-insensitive): 2
“`

7. HashSet 与其他集合类型的比较

了解 HashSet 的优缺点有助于您在众多集合类型中做出正确的选择。

  • List<T>:

    • 优点: 有序,可以通过索引访问,方便插入和删除指定位置的元素。
    • 缺点: 查找 (Contains)、添加和删除元素的性能通常是 O(n),特别是在列表较大时。允许重复元素。
    • 何时使用: 需要有序集合,需要按索引访问,需要频繁在特定位置插入/删除元素,允许重复元素。
  • Dictionary<TKey, TValue>:

    • 优点: 存储键值对,通过键进行极速查找(平均 O(1))。键是唯一的。
    • 缺点: 存储的是键值对,而不是独立的元素集合。
    • 何时使用: 需要存储键值对,通过唯一的键来快速查找对应的值。可以将 HashSet<T> 看作是 Dictionary<T, bool>Dictionary<T, object> 的一种特化形式,只关心键的存在性而不关心值。
  • SortedSet<T>:

    • 优点: 存储唯一的元素,并且元素是始终保持排序的。提供了高效的查找、添加、删除操作(通常是 O(log n)),以及范围查询等功能。
    • 缺点: 性能不如 HashSet<T> 的 O(1) 平均性能。需要元素类型实现 IComparable<T> 或提供 IComparer<T>
    • 何时使用: 需要存储唯一的元素,并且需要元素保持排序,或者需要执行范围查询。
  • HashSet<T>:

    • 优点: 存储唯一的元素。极速的添加、删除、查找操作(平均 O(1))。提供了强大的集合运算方法。
    • 缺点: 无序。不能通过索引访问。
    • 何时使用: 需要存储唯一的元素,并且对添加、删除、查找操作的性能要求极高,同时不关心元素的顺序。

总而言之,HashSet<T> 是处理需要唯一性、高速查找和集合运算场景的首选。

8. 实际应用场景举例

  • 查找列表中的重复项或获取唯一项: 将列表中的所有元素添加到 HashSet 中,HashSet.Count 就是唯一元素的数量。与原列表数量对比即可知是否有重复项。
  • 实现过滤器或黑名单: 将需要过滤的项存储在 HashSet 中,当处理新项时,使用 Contains 快速判断是否应该过滤。
  • 数据去重: 读取多个数据源的数据,将它们全部添加到同一个 HashSet 中,最终 HashSet 中就只剩下所有来源的唯一数据。
  • 图算法: 在遍历图时,可以使用 HashSet 来跟踪已经访问过的节点,以避免无限循环或重复处理。
  • 查找共同好友、共同兴趣等: 利用 IntersectWith 方法快速找到两个用户集合的交集。
  • 权限管理: 存储用户拥有的权限集合,使用 ContainsOverlaps 检查用户是否具备特定权限或权限组。

9. 总结

HashSet<T> 是 C# 中一个功能强大且性能卓越的集合类型,特别适合用于存储和管理唯一元素的场景。其基于散列的内部机制保证了添加、删除和查找操作在平均情况下具有极高的效率(O(1))。

理解 HashSet 的关键在于理解唯一性是基于元素的 Equals()GetHashCode() 方法的。当处理自定义对象时,务必正确重写这两个方法,以确保集合的正确行为。

通过 ISet<T> 接口,HashSet 还提供了丰富的集合操作,如并集、交集、差集等,这使得它在处理集合关系问题时非常方便。

掌握 HashSet<T> 的使用,将显著提升您在处理需要高效管理唯一数据集合时的编程效率和程序性能。请在实际项目中尝试使用它,体验其带来的便利和速度!

希望这篇详细教程对您理解和使用 C# HashSet<T> 有所帮助!


发表评论

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

滚动至顶部