好的,在 C# 中实现 List<T> 去重有多种方法,每种方法都有其特定的应用场景和性能特点。下面我将为您详细介绍几种高效且常用的方法,并从性能和功能角度进行分析。
![图片[1]_C#实现List<T>去重的多种高效方法_知途无界](https://zhituwujie.com/wp-content/uploads/2025/12/d2b5ca33bd20251210095327.png)
核心思路分类
- 使用 LINQ (简洁,适合大多数场景)
- 使用 HashSet (高性能,适合大型列表和复杂类型)
- 使用传统循环 (最可控,可自定义逻辑)
- 使用 DistinctBy (根据特定属性去重,.NET 6+)
方法一:使用 LINQ 的 Distinct() 方法 (最简洁)
这是最常用、代码最简洁的方法,适用于快速去重。
原理:Distinct() 方法返回一个序列,其中包含源序列中的唯一元素(基于元素的默认相等比较器)。
代码示例:
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
List<int> numbers = new List<int> { 1, 2, 2, 3, 4, 4, 5 };
// 使用 Distinct() 去重,并转换回 List
List<int> distinctNumbers = numbers.Distinct().ToList();
Console.WriteLine(string.Join(", ", distinctNumbers)); // 输出: 1, 2, 3, 4, 5
}
}
优点:
- 极其简洁,一行代码搞定。
- 可读性强。
缺点:
- 对于自定义类,需要提供正确的
IEqualityComparer<T>或使用默认的基于所有属性的比较(通常不适用于自定义类)。 - 性能不是最优的,因为它内部使用了延迟执行和哈希集,但有额外的封装开销。
适用场景:快速对简单数据类型(int, string, Guid 等)或已正确实现了相等比较器的类型进行去重。
方法二:使用 HashSet<T> (高性能首选)
HashSet<T> 本身就是为存储唯一元素设计的,利用其特性进行去重是性能最高的方法之一。
原理:遍历列表,将每个元素尝试添加到 HashSet<T> 中。由于 HashSet<T> 不允许重复,重复的元素会被自动忽略。最后再将 HashSet<T> 转换回 List<T>。
代码示例:
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<int> numbers = new List<int> { 1, 2, 2, 3, 4, 4, 5 };
// 使用 HashSet 去重
HashSet<int> set = new HashSet<int>(numbers);
List<int> distinctNumbers = set.ToList();
Console.WriteLine(string.Join(", ", distinctNumbers)); // 输出: 1, 2, 3, 4, 5
}
}
更高效的原地修改方式 (避免创建新列表):
如果你不需要保留原始列表,可以直接清空并重新填充,减少内存分配。
List<int> numbers = new List<int> { 1, 2, 2, 3, 4, 4, 5 };
HashSet<int> set = new HashSet<int>(numbers);
numbers.Clear();
numbers.AddRange(set); // AddRange 比循环 Add 更高效
Console.WriteLine(string.Join(", ", numbers)); // 输出: 1, 2, 3, 4, 5
优点:
- 性能极高,特别是对于大型列表。
HashSet<T>的查找和插入操作的平均时间复杂度接近 O(1)。 - 代码清晰易懂。
缺点:
- 对于自定义类,同样需要实现
GetHashCode()和Equals(object)方法,或者使用自定义的IEqualityComparer<T>。 - 会改变元素的顺序(
HashSet<T>不保证顺序)。如果需要保持原始顺序,请看下面的进阶版。
保持顺序的 HashSet 用法:HashSet<T> 在 .NET Framework 中不保证顺序,但在 .NET Core/.NET 5+ 中,HashSet 会保持元素的插入顺序。为了在所有版本中确保顺序,可以结合 LinkedHashSet 的思想(在 C# 中可用 Dictionary 模拟)或使用 Distinct()(它保证顺序)。
适用场景:追求极致性能的去重操作,尤其是对顺序无要求或在使用现代 .NET 运行时。
方法三:使用传统循环与 HashSet (手动控制,保持顺序)
这种方法结合了 HashSet 的高性能和循环的灵活性,可以保持元素的第一次出现的顺序。
原理:遍历列表,用一个 HashSet<T> 来记录已经出现过的元素。对于每个元素,如果它不在 HashSet<T> 中,就将其加入结果列表和 HashSet<T> 中。
代码示例:
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<int> numbers = new List<int> { 1, 2, 2, 3, 4, 4, 5 };
List<int> distinctOrderedNumbers = new List<int>();
HashSet<int> seen = new HashSet<int>();
foreach (int number in numbers)
{
// 如果 HashSet 中没有这个元素,说明是第一次遇到
if (seen.Add(number)) // Add 方法在元素已存在时返回 false
{
distinctOrderedNumbers.Add(number);
}
}
Console.WriteLine(string.Join(", ", distinctOrderedNumbers)); // 输出: 1, 2, 3, 4, 5 (保持原顺序)
}
}
优点:
- 性能高(O(n) 时间复杂度)。
- 保持元素的原始顺序。
- 完全可控,可以在循环中加入任何自定义逻辑。
缺点:
- 代码量稍多。
适用场景:最常用的高性能去重场景,尤其当需要保持元素顺序时。这是很多框架和库的 Distinct 方法内部可能采用的实现方式。
方法四:根据特定属性去重 (DistinctBy, .NET 6+)
如果要基于对象的某个或多个属性去重(例如,一个 Person 列表,只根据 Id 去重),Distinct() 就不够用了。.NET 6 引入了 DistinctBy 方法完美解决这个问题。
原理:允许你指定一个键选择器函数,该方法会根据这个键的唯一性来判断元素是否重复。
代码示例:
假设有一个 Person 类:
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
}
使用 DistinctBy 根据 Id 去重:
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
List<Person> people = new List<Person>
{
new Person { Id = 1, Name = "Alice" },
new Person { Id = 2, Name = "Bob" },
new Person { Id = 1, Name = "Charlie" }, // Id=1 重复
new Person { Id = 3, Name = "David" }
};
// 根据 Id 属性去重
List<Person> distinctPeople = people.DistinctBy(p => p.Id).ToList();
foreach (var person in distinctPeople)
{
Console.WriteLine($"Id: {person.Id}, Name: {person.Name}");
}
// 输出:
// Id: 1, Name: Alice
// Id: 2, Name: Bob
// Id: 3, Name: David
// 注意:保留了第一个遇到的 Id=1 的元素 (Alice)
}
}
优点:
- 功能强大,可以轻松实现复杂的去重逻辑。
- 代码简洁明了。
缺点:
- 需要 .NET 6 或更高版本。
适用场景:需要根据对象的一个或多个特定字段进行去重。
性能对比与总结
| 方法 | 性能 | 保持顺序 | 代码简洁性 | 适用场景 |
|---|---|---|---|---|
**LINQ Distinct()** | 良好 | 是 | 非常高 | 快速开发,简单类型,无需极致性能 |
**HashSet<T> 直接构造** | 最佳 | 否 (.NET Core+ 是) | 高 | 追求性能,对顺序无要求或在新版.NET |
**循环 + HashSet<T>** | 最佳 | 是 | 中等 | 通用高性能场景,需保序 |
**DistinctBy (NET 6+)** | 良好 | 是 | 非常高 | 根据特定属性去重 |
如何选择?
- 日常快速开发,简单类型:直接用 **
list.Distinct().ToList()**。 - 追求极致性能,且不关心顺序:使用 **
new HashSet<T>(list).ToList()**。 - 追求极致性能,且必须保持顺序:使用 **循环 +
HashSet<T> 或 list.Distinct().ToList()**(Distinct内部实现类似循环+哈希表,并保证顺序)。 - 需要根据对象属性去重 (且环境是 .NET 6+):毫不犹豫选择 **
list.DistinctBy(x => x.Property).ToList()**。 - 自定义去重逻辑(如复杂条件判断):使用 传统循环,在其中编写你的判断规则。
对于大多数应用来说,方法三(循环 + HashSet) 是实现“高效且保持顺序去重”的最可靠和通用的选择。

























暂无评论内容