linq distinct 不够用了!
问题引出:在实际中遇到一个问题,要进行集合去重,集合内存储的是引用类型,需要根据id进行去重。这个时候linq 的distinct 就不够用了,对于引用类型,它直接比较地址。测试数据如下:
1 2 3 4 5 6 7 8 9 10 11 12 | class Person { public int ID { get ; set ; } public string Name { get ; set ; } } List<Person> list = new List<Person>() { new Person(){ID=1,Name= "name1" }, new Person(){ID=1,Name= "name1" }, new Person(){ID=2,Name= "name2" }, new Person(){ID=3,Name= "name3" } }; |
我们需要根据Person 的 ID 进行去重。当然使用linq Distinct 不满足,还是有办法实现的,通过GroupBy先分一下组,再取第一个数据即可。例如:
1 | list.GroupBy(x => x.ID).Select(x => x.FirstOrDefault()).ToList() |
通常通过GroupBy去实现也是可以的,毕竟在内存操作还是很快的。但这里我们用别的方式去实现,并且找到最好的实现方式。
一、通过IEqualityComparer接口
IEnumerable<T> 的扩展方法 Distinct 定义如下:
1 2 | public static IEnumerable<TSource> Distinct<TSource>( this IEnumerable<TSource> source); public static IEnumerable<TSource> Distinct<TSource>( this IEnumerable<TSource> source, IEqualityComparer<TSource> comparer); |
可以看到,Distinct方法有一个参数为 IEqualityComparer<T> 的重载。该接口的定义如下:
1 2 3 4 5 6 | // 类型参数 T: 要比较的对象的类型。 public interface IEqualityComparer<T> { bool Equals(T x, T y); int GetHashCode(T obj); } |
通过实现这个接口我们就可以实现自己的比较器,定义自己的比较规则了。
这里有一个问题,IEqualityComparer<T> 的 T 是要比较的对象的类型,在这里就是 Person,那这里如何去获得 Person 的属性 id呢?或者说,对于任何类型,我如何知道要比较的是哪个属性?答案就是:委托。通过委托,要比较什么属性由外部指定。这也是linq 扩展方法的设计,参数都是委托类型的,也就是规则由外部定义,内部只负责调用。ok,我们看最后实现的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | //通过继承EqualityComparer类也是一样的。 class CustomerEqualityComparer<T,V> : IEqualityComparer<T> { private IEqualityComparer<V> comparer; private Func<T, V> selector; public CustomerEqualityComparer(Func<T, V> selector) : this (selector,EqualityComparer<V>.Default) { } public CustomerEqualityComparer(Func<T, V> selector, IEqualityComparer<V> comparer) { this .comparer = comparer; this .selector = selector; } public bool Equals(T x, T y) { return this .comparer.Equals( this .selector(x), this .selector(y)); } public int GetHashCode(T obj) { return this .comparer.GetHashCode( this .selector(obj)); } } |
(补充1)之前没有把扩展方法贴出来,而且看到有朋友提到比较字符串忽略大小写的问题(其实上面有两个构造函数就可以解决这个问题)。这里扩展方法可以写为:
1 2 3 4 5 6 7 8 9 10 11 12 | static class EnumerableExtention { public static IEnumerable<TSource> Distinct<TSource,TKey>( this IEnumerable<TSource> source, Func<TSource,TKey> selector) { return source.Distinct( new CustomerEqualityComparer<TSource,TKey>(selector)); } //4.0以上最后一个参数可以写成默认参数 EqualityComparer<T>.Default,两个扩展Distinct可以合并为一个。 public static IEnumerable<TSource> Distinct<TSource, TKey>( this IEnumerable<TSource> source, Func<TSource, TKey> selector, IEqualityComparer<TKey> comparer) { return source.Distinct( new CustomerEqualityComparer<TSource, TKey>(selector,comparer)); } } |
例如,要根据Person的Name忽略大小写比较,就可以写成:
1 | list.Distinct(x => x.Name,StringComparer.CurrentCultureIgnoreCase).ToList(); //StringComparer实现了IEqualityComaparer<string> 接口 |
二、通过哈希表。第一种做法的缺点是不仅要定义新的扩展方法,还要定义一个新类。能不能只有一个扩展方法就搞定?可以,通过Dictionary就可以搞定(有HashSet就用HashSet)。实现方式如下:
1 2 3 4 5 6 7 8 9 10 11 | public static IEnumerable<TSource> Distinct<TSource,TKey>( this IEnumerable<TSource> source, Func<TSource,TKey> selector) { Dictionary<TKey, TSource> dic = new Dictionary<TKey, TSource>(); foreach ( var s in source) { TKey key = selector(s); if (!dic.ContainsKey(key)) dic.Add(key, s); } return dic.Select(x => x.Value); } |
三、重写object方法。能不能连扩展方法也不要了?可以。我们知道 object 是所有类型的基类,其中有两个虚方法,Equals、GetHashCode,默认情况下,.net 就是通过这两个方法进行对象间的比较的,那么linq 无参的Distinct 是不是也是根据这两个方法来进行判断的?我们在Person里 override 这两个方法,并实现自己的比较规则。打上断点调试,发现在执行Distinct时,是会进入到这两个方法的。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | class Person { public int ID { get ; set ; } public string Name { get ; set ; } public override bool Equals( object obj) { Person p = obj as Person; return this .ID.Equals(p.ID); } public override int GetHashCode() { return this .ID.GetHashCode(); } } |
在我的需求里,是根据id去重的,所以第三种方式提供了最优雅的实现。如果是其它情况,用前面的方法更通用。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构