平时做技术实践时,很多问题不是概念不会,而是细节没串起来。拿“C#中List优雅去重的几种做法”来说,它看着像小点,放到项目里常会牵出环境、配置、兼容性和维护成本。下面按实际采用顺序,把思路、关键写法和容易踩坑的地方讲清楚,便于大家直接对照操作。
理解这一步时,在日常开发里,我们经常需处理集合中的重复数据。本文将全面介绍C#中List去重的各种优雅方式,帮助你在不同场景下选择最合适的方案。
前言
数据去重是数据处理中的常用需求,主要目的包括:
· 避免数据重复导致的统计错误
· 减少内存占用和提高性能
· 保证数据的唯一性约束
· 提升数据质量和用户体验
理解这一步时,本文将带你全面掌握C#中List去重的各种技巧,从基础用法到高级方案,应有尽有!
基础值类型去重
采用 LINQ 的 Distinct() 方法
// 整数去重
Listnumbers = new List { 1, 2, 2, 3, 4, 4, 5 };
ListdistinctNumbers = numbers.Distinct().ToList();
Console.WriteLine(string.Join(", ", distinctNumbers));
// 输出: 1, 2, 3, 4, 5
// 字符串去重
Listnames = new List { "Alice", "Bob", "Alice", "Charlie" };
ListdistinctNames = names.Distinct().ToList();
Console.WriteLine(string.Join(", ", distinctNames));
// 输出: Alice, Bob, Charlie
自定义对象去重
首先定义示例类
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public override string ToString() => $"Id: {Id}, Name: {Name}, Age: {Age}";
}
// 准备测试数据
Listpeople = new List
{
new Person { Id = 1, Name = "Alice", Age = 25 },
new Person { Id = 1, Name = "Alice", Age = 26 }, // 重复ID
new Person { Id = 2, Name = "Bob", Age = 30 },
new Person { Id = 3, Name = "Charlie", Age = 35 }
};
方法一:重写 Equals 和 GetHashCode
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
// 基于Id去重
public override bool Equals(object obj)
{
return obj is Person person && Id == person.Id;
}
public override int GetHashCode()
{
return Id.GetHashCode();
}
}
// 使用
ListdistinctPeople = people.Distinct().ToList();
foreach (var person in distinctPeople)
{
Console.WriteLine(person);
}
方法二:采用自定义比较器(建议)
public class PersonComparer : IEqualityComparer
{
private readonly bool _compareName;
public PersonComparer(bool compareName = false)
{
_compareName = compareName;
}
public bool Equals(Person x, Person y)
{
if (ReferenceEquals(x, y)) return true;
if (x is null || y is null) return false;
if (_compareName)
return x.Id == y.Id && x.Name == y.Name;
else
return x.Id == y.Id;
}
public int GetHashCode(Person obj)
{
if (_compareName)
return HashCode.Combine(obj.Id, obj.Name);
else
return obj.Id.GetHashCode();
}
}
// 使用自定义比较器
ListdistinctById = people.Distinct(new PersonComparer()).ToList();
ListdistinctByIdAndName = people.Distinct(new PersonComparer(true)).ToList();
Console.WriteLine("基于ID去重:");
foreach (var person in distinctById)
{
Console.WriteLine($" {person}");
}
Console.WriteLine("基于ID和Name去重:");
foreach (var person in distinctByIdAndName)
{
Console.WriteLine($" {person}");
}
高性能去重方案
采用 HashSet(建议用来大数据量)
// 基本值类型 - 最高效
Listnumbers = new List { 1, 2, 2, 3, 4, 4, 5 };
HashSetuniqueNumbers = new HashSet (numbers);
ListdistinctNumbers = uniqueNumbers.ToList();
// 自定义对象
HashSetuniquePeople = new HashSet (people, new PersonComparer());
ListdistinctPeople = uniquePeople.ToList();
// 内联比较器(无需定义类,简洁用法)
var distinctPeopleInline = new HashSet(people,
EqualityComparer.Create((x, y) => x?.Id == y?.Id,
x => x?.Id.GetHashCode() ?? 0))
.ToList();
Console.WriteLine($"原始数据: {people.Count} 条");
Console.WriteLine($"去重后: {distinctPeople.Count} 条");
.NET 6+ 的新特性
采用 DistinctBy 方法(最优雅的方式)
// 按单个属性去重
ListdistinctById = people.DistinctBy(p => p.Id).ToList();
// 按多个属性去重
ListdistinctByIdAndName = people
.DistinctBy(p => new { p.Id, p.Name })
.ToList();
// 使用元组(性能更好)
ListdistinctByTuple = people
.DistinctBy(p => (p.Id, p.Name))
.ToList();
// 复杂条件去重
ListdistinctByCondition = people
.DistinctBy(p => new {
Category = p.Age < 30 ? "Young" : "Adult",
p.Name
})
.ToList();
Console.WriteLine("使用DistinctBy按ID去重:");
foreach (var person in distinctById)
{
Console.WriteLine($" {person}");
}
采用 GroupBy 方法
// 按ID分组取第一个
ListdistinctPeople = people
.GroupBy(p => p.Id)
.Select(g => g.First())
.ToList();
// 按多个属性分组
ListdistinctByMultiple = people
.GroupBy(p => new { p.Id, p.Name })
.Select(g => g.First())
.ToList();
// 取最后一个或特定元素
ListdistinctLast = people
.GroupBy(p => p.Id)
.Select(g => g.Last())
.ToList();
// 按条件选择(如年龄最大的)
ListdistinctOldest = people
.GroupBy(p => p.Id)
.Select(g => g.OrderByDescending(p => p.Age).First())
.ToList();
Console.WriteLine("取每个ID中年龄最大的:");
foreach (var person in distinctOldest)
{
Console.WriteLine($" {person}");
}
优雅的扩展方法封装
新建扩展方法类
public static class ListExtensions
{
///
/// 根据指定键选择器去重
///
public static ListDistinctBy (this IEnumerable source, Func keySelector)
{
return source.DistinctBy(keySelector, null);
}
///
/// 根据指定键选择器和比较器去重
///
public static ListDistinctBy (this IEnumerable source, Func keySelector, IEqualityComparer comparer)
{
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(keySelector);
var seen = new HashSet(comparer);
return source.Where(item => seen.Add(keySelector(item))).ToList();
}
///
/// 高性能去重(使用HashSet)
///
public static ListFastDistinct (this IEnumerable source)
{
return new HashSet(source).ToList();
}
///
/// 高性能去重(使用HashSet和比较器)
///
public static ListFastDistinct (this IEnumerable source, IEqualityComparer comparer)
{
return new HashSet(source, comparer).ToList();
}
}
采用扩展方法
// 使用扩展方法
var distinctById = people.DistinctBy(p => p.Id);
var distinctByName = people.DistinctBy(p => p.Name);
var distinctByMultiple = people.DistinctBy(p => new { p.Id, p.Name });
// 高性能去重
var fastDistinct = people.FastDistinct();
var fastDistinctWithComparer = people.FastDistinct(new PersonComparer());
Console.WriteLine("使用扩展方法去重:");
Console.WriteLine($"按ID去重: {distinctById.Count} 条记录");
Console.WriteLine($"高性能去重: {fastDistinct.Count} 条记录");
性能比较和选择指南
为便于帮助大家选择最佳方案,我整理了以下性能对比表格:
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 | 代码简洁性 | 建议指数 |
|---|---|---|---|---|---|
| HashSet | O(n) | O(n) | 大数据量,性能要求高 | ||
| Distinct() | O(n) | O(n) | 一般场景 | ||
| DistinctBy() | O(n) | O(n) | .NET 6+,按属性去重 | ||
| GroupBy | O(n) | O(n) | 需分组逻辑时 | ||
| 扩展方法 | O(n) | O(n) | 代码复用和封装 |
轻松性能测试示例
public class PerformanceTest
{
public static void TestDistinctMethods()
{
// 生成测试数据
var largeList = Enumerable.Range(0, 100000)
.Select(i => new Person { Id = i % 1000, Name = $"Person{i}" })
.ToList();
var stopwatch = Stopwatch.StartNew();
// 测试不同方法
var result1 = largeList.Distinct().ToList();
Console.WriteLine($"Distinct(): {stopwatch.ElapsedMilliseconds}ms");
stopwatch.Restart();
var result2 = largeList.DistinctBy(p => p.Id).ToList();
Console.WriteLine($"DistinctBy(): {stopwatch.ElapsedMilliseconds}ms");
stopwatch.Restart();
var result3 = new HashSet(largeList).ToList();
Console.WriteLine($"HashSet: {stopwatch.ElapsedMilliseconds}ms");
stopwatch.Restart();
var result4 = largeList.GroupBy(p => p.Id).Select(g => g.First()).ToList();
Console.WriteLine($"GroupBy: {stopwatch.ElapsedMilliseconds}ms");
}
}
实际应用场景
场景一:数据库查询结果去重
// 模拟数据库查询结果
var databaseResults = new List
{
new Person { Id = 1, Name = "Alice", Age = 25 },
new Person { Id = 1, Name = "Alice", Age = 25 }, // 重复记录
new Person { Id = 2, Name = "Bob", Age = 30 },
new Person { Id = 3, Name = "Charlie", Age = 35 }
};
// 去重处理
var cleanResults = databaseResults
.DistinctBy(p => p.Id)
.ToList();
Console.WriteLine($"数据库查询去重: {databaseResults.Count} → {cleanResults.Count} 条记录");
场景二:API响应数据合并去重
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
public class ApiService
{
public ListMergeProducts(List localData, List apiData)
{
// 合并数据并去重
var mergedData = localData
.Concat(apiData)
.DistinctBy(p => p.Id)
.ToList();
Console.WriteLine($"数据合并: {localData.Count} + {apiData.Count} → {mergedData.Count} 条记录");
return mergedData;
}
}
场景三:复杂业务逻辑去重
public class Order
{
public int CustomerId { get; set; }
public int ProductId { get; set; }
public DateTime OrderDate { get; set; }
public decimal TotalAmount { get; set; }
}
public class OrderService
{
public ListRemoveDuplicateOrders(List orders)
{
// 基于多个业务规则去重:同一客户、同一产品、同一天只保留金额最大的订单
return orders
.GroupBy(o => new { o.CustomerId, o.ProductId, o.OrderDate.Date })
.Select(g => g.OrderByDescending(o => o.TotalAmount).First())
.ToList();
}
}
最佳实践和建议
选择策略
- 根据数据量选择方法:
· 小数据量(< 1000):使用 Distinct() 或 DistinctBy()
· 大数据量(≥ 1000):采用 HashSet - 考虑比较逻辑复杂度:
· 轻松比较:采用 Lambda 表达式
· 复杂比较:采用自定义比较器 - 空值安全处理:
public class SafePersonComparer : IEqualityComparer
{
public bool Equals(Person x, Person y)
{
if (x is null && y is null) return true;
if (x is null || y is null) return false;
return x.Id == y.Id && x.Name == y.Name;
}
public int GetHashCode(Person obj)
{
return obj?.Id.GetHashCode() ?? 0;
}
}
- 线程安全考虑:
· 单线程:采用普通方法
· 多线程:考虑采用 ConcurrentDictionary 或适当的同步机制
总结
结合项目来看,借助本文的学习,相信你已经掌握了C#中List去重的各种优雅方式。下面是我的终极选择建议:
实际处理时,· .NET 6+ 项目:优先采用 DistinctBy(),代码最简洁
· 高性能需求:采用 HashSet 构造器
· 复杂比较逻辑:采用自定义 IEqualityComparer
· 代码复用:封装为扩展方法
· 维护性:为自定义比较器添加单元测试
理解这一步时,总的来说,C# List 去重适合结合实际项目边做边理解。先抓住核心思路,再逐步补上细节和边界处理,最后效果会更稳定,也更容易复用。
