C# LINQ Distinct去重方法详解:按字段去重与自定义比较器完整指南

Distinct 默认去重机制:值类型与引用类型的核心差异
在C#中直接对 List 这类自定义引用类型集合调用 .Distinct() 方法,通常无法实现按字段内容去重的效果。这是因为默认的 Distinct 操作比较的是对象的内存引用地址,而非对象内部属性的值。即使两个 Person 实例的 Name 和 Age 属性完全相同,只要它们是不同的对象实例,就会被视为不同元素而保留。
典型问题表现:调用 .Distinct() 后集合元素数量未减少,调试时发现数据重复问题依然存在。
- 基础值类型(如
int、double)和string可直接使用.Distinct()实现去重 - 自定义类必须显式定义比较逻辑,否则默认基于引用的比较行为无法满足业务需求
- 字符串虽然是引用类型,但因其重写了
Equals和GetHashCode方法,故List能够正确工作.Distinct()
Distinct方法对引用类型默认执行对象引用去重,需自定义比较逻辑;值类型和已重写Equals的引用类型(如string)可直接使用;单字段去重推荐GroupBy+First组合,多字段去重可采用匿名类型+DistinctBy(.NET 6+)或GroupBy方案,复杂业务场景需实现IEqualityComparer接口。
按单个属性去重:GroupBy + First 组合方案
无需编写比较器代码,也不必创建额外类,这种方法特别适合快速按 Id、Name 等唯一性字段筛选“首次出现的记录”。
应用示例:从产品列表中提取每个 CategoryId 分类下的第一个 Product 对象
var uniqueByCategory = list
.GroupBy(x => x.CategoryId)
.Select(g => g.First())
.ToList();
GroupBy操作按指定字段分组后,每组至少包含一个元素,使用First()选取首个元素,逻辑清晰易懂- 性能方面虽略低于
Distinct配合自定义比较器(需完成分组操作),但开发效率更高且不易出错 - 若需获取“最新记录”(如按
CreatedTime降序排列的首条),可将First()替换为OrderByDescending(x => x.CreatedTime).First()
按多个属性组合去重:匿名类型与Distinct的优雅方案
当需要基于 FirstName 和 LastName 等多个字段的组合进行去重时,利用匿名类型自动实现的相等性比较是最简洁的方案,无需手动编写比较器。
代码示例:去除姓名(姓+名)重复的用户记录
var uniqueByName = users
.DistinctBy(u => new { u.FirstName, u.LastName })
.ToList();
⚠️ 重要说明:DistinctBy 是 .NET 6 及以上版本新增的扩展方法(位于 System.Linq 命名空间),旧版本框架需使用 GroupBy 替代实现:
var uniqueByName = users
.GroupBy(u => new { u.FirstName, u.LastName })
.Select(g => g.First())
.ToList();
- 匿名类型的属性名称和大小写必须完全一致,
new { F = u.FirstName }与new { FirstName = u.FirstName }被视为不同的类型 - 当字段值为
null时,匿名类型能够正确处理空值比较 - 此方案不支持动态字段数量(如运行时传入字段名列表),此类场景必须通过自定义
IEqualityComparer实现
自定义比较器实现:IEqualityComparer 接口的完整控制方案
当业务需要复杂比较逻辑(如忽略大小写、按子字符串匹配、多级优先级判断)或需兼容旧框架(.NET Framework / .NET 5及以下)时,必须通过实现 IEqualityComparer 接口来提供完全可控的比较机制。
示例:实现按 Code 字段忽略大小写的产品去重比较器
public class CodeComparer : IEqualityComparer{ public bool Equals(Product x, Product y) => x?.Code?.Equals(y?.Code, StringComparison.OrdinalIgnoreCase) == true; public int GetHashCode(Product obj) => obj?.Code?.ToLowerInvariant().GetHashCode() ?? 0; }
调用方式:
var unique = products.Distinct(new CodeComparer()).ToList();
GetHashCode方法的实现必须与Equals方法的逻辑严格一致,否则Distinct可能出现漏判或误判- 必须显式处理空值(
null)情况,避免引发NullReferenceException异常 - 比较器实例应当复用,避免在循环或高频调用路径中重复创建新实例
最易被忽视的关键点:Distinct 操作采用延迟执行模式,但去重的准确性完全依赖于所提供的比较逻辑是否全面覆盖业务场景。例如仅按 Email 字段去重却未处理前后空格或大小写差异,可能导致数据重复问题被隐藏。
