理解LINQ的Distinct方法及其默认行为
在C#中,LINQ(语言集成查询)的`Distinct`方法是用于从序列中返回不重复元素的便捷操作。它的基本形式非常简单,直接对集合调用即可。默认情况下,`Distinct`方法使用元素的默认相等比较器(`DefaultEqualityComparer`)来比较值类型和判断引用类型的引用是否相等。对于诸如`int`、`string`等基本类型,它可以直接工作。
例如,对于一个整数集合,去除重复项非常简单:
List<int> numbers = new List<int> { 1, 2, 2, 3, 4, 4, 5 };IEnumerable<int> uniqueNumbers = numbers.Distinct();// 输出结果:1, 2, 3, 4, 5默认行为的局限性
然而,当处理自定义的类(引用类型)时,`Distinct`的默认行为可能无法达到预期效果。因为默认比较器比较的是对象的引用(即内存地址),而不是对象内部的值。即使两个对象的所有属性值都相同,只要它们是不同的实例,就会被视为不同的元素。
public class Product{ public int Id { get; set; } public string Name { get; set; }}List<Product> products = new List<Product>{ new Product { Id = 1, Name = Apple }, new Product { Id = 1, Name = Apple } // 这是一个新的对象实例};IEnumerable<Product> distinctProducts = products.Distinct();// distinctProducts 仍然包含两个对象,因为它们是不同的实例为自定义类型实现高效的Distinct去重
要让`Distinct`方法根据对象的属性值进行去重,我们需要自定义相等比较的逻辑。这在C#中主要有两种实现方式:重写对象的`Equals`和`GetHashCode`方法,或者提供一个自定义的`IEqualityComparer`实现。
方法一:重写Equals和GetHashCode方法
这是最直接的方法。通过在你自定义的类中重写`Equals`和`GetHashCode`方法,你可以定义两个对象在什么情况下被视为相等。`Distinct`方法会使用这些重写后的方法来比较对象。
public class Product{ public int Id { get; set; } public string Name { get; set; } public override bool Equals(object obj) { if (obj is Product product) { return this.Id == product.Id && this.Name == product.Name; } return false; } public override int GetHashCode() { // 使用HashCode.Combine来生成复合值的哈希码,这是推荐的做法 return HashCode.Combine(Id, Name); }}完成重写后,再次调用`Distinct()`,就能正确地去重了。这种方法的好处是,该类的相等性定义在整个应用程序中都是一致的。
方法二:使用自定义的IEqualityComparer
当你无法修改类的源代码(例如,类来自第三方库),或者你需要针对不同的场景使用不同的去重逻辑时,自定义比较器是更好的选择。你需要创建一个实现`IEqualityComparer`接口的类。
public class ProductEqualityComparer : IEqualityComparer<Product>{ public bool Equals(Product x, Product y) { if (ReferenceEquals(x, y)) return true; if (x is null || y is null) return false; return x.Id == y.Id && x.Name == y.Name; } public int GetHashCode(Product obj) { if (obj is null) return 0; return HashCode.Combine(obj.Id, obj.Name); }}在使用`Distinct`方法时,将比较器的实例作为参数传入:
IEnumerable<Product> distinctProducts = products.Distinct(new ProductEqualityComparer());高级用法:使用匿名类型和Select进行属性去重
有时,你可能只希望根据对象的某个或某几个属性进行去重,而不是整个对象。一个非常高效和简洁的方法是结合LINQ的`Select`和匿名类型。
这种方法的原理是:先使用`Select`将对象投影到一个匿名类型(该匿名类型只包含需要去重的属性),然后对匿名类型调用`Distinct`,因为匿名类型会自动实现基于值的相等比较。最后,如果需要,可以再转换回原始类型。
// 只根据Id属性去重var distinctById = products .Select(p => new { p.Id }) // 投影到只有Id的匿名对象 .Distinct();// 根据Id和Name两个属性去重,并转换回Product对象(注意:这里可能会丢失未选中的属性)var distinctByProperties = products .GroupBy(p => new { p.Id, p.Name }) // 使用GroupBy根据匿名类型分组 .Select(g => g.First()); // 取每组的第一个元素作为代表这种方法非常灵活,无需创建自定义比较器或修改类本身,特别适用于临时性的、基于特定属性的去重需求。
性能考量与最佳实践
在使用`Distinct`方法时,性能是一个重要的考虑因素。
哈希码的重要性
无论是重写`GetHashCode`还是实现`IEqualityComparer.GetHashCode`,生成一个分布良好的哈希码都至关重要。一个好的哈希码应能最大程度地减少冲突(即不同对象产生相同哈希码的情况),因为哈希冲突会降低`Distinct`内部使用的哈希表的查找效率。
.NET Core 2.1及以上版本引入了`HashCode.Combine`方法,它是生成复合哈希码的推荐方式,因为它能有效地组合多个字段的哈希值。
选择合适的去重策略
- 对于简单值类型集合:直接使用`Distinct()`,性能最优。
- 对于自定义类型,且有固定的去重逻辑:重写类的`Equals`和`GetHashCode`方法。
- 对于无法修改的类或需要灵活多变的去重逻辑:使用自定义的`IEqualityComparer`。
- 对于临时的、基于部分属性的去重:使用匿名类型和`GroupBy`的方法,代码简洁且意图明确。
通过理解`Distinct`方法的工作原理并结合上述实践指南,你可以在C#中高效、准确地对各种集合进行去重操作,从而编写出更清晰、更健壮的代码。
2万+

被折叠的 条评论
为什么被折叠?



