利用Distinct()内置方法对List集合的去重问题详解
作者:cmm123123 发布时间:2023-01-31 00:45:30
前言
说到对集合去重处理,第一时间想到的肯定是Linq的Distinct扩展方式,对于一般的值类型集合去重,很好处理,直接list.Distinct()即可。但是如果想要对一个引用类型的集合去重(属性值都相同就认为重复),就会发现,直接Distinct()是不行的
先来看看泛型链表 List<T> 的定义:
public class List<T> : IList<T>, ICollection<T>, IList, ICollection, IReadOnlyList<T>, IReadOnlyCollection<T>, IEnumerable<T>, IEnumerable
可见它实现了 IEnumerable<T>,而IEnumerable<T>规定了Distinct方法。
使用这个方法时要注意:
(1)该方法并不会改变原来的链表;
(2)该方法返回一个对象(假设叫做dis),通过该对象可以枚举原链表中的非重复元素,但是并没有把非重复元素复制一份到新的对象中(连签拷贝也没有)
(3)由于(2),在枚举dis时,始终是依赖于原有链表,所以如果在获得dis后,又更新了原有链表,那么使用dis枚举将会使用原有链表的最新状态。
var list=new List<SampleVersionDto>()///表明具有重复值得集合
有时候Distinct()不能对引用类型去重时 我们就要自定义了 自定义代码如下:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
}
var list = new List<User>()
{
new User() { Id = 1, Name = "张三" } ,
new User() { Id = 1, Name = "张三" } ,
new User() { Id = 3, Name = "李四" } ,
};
var newList1 = list.Distinct().ToList();
运行上述代码会发现,并不是预期想要的结果,newList1还是有3个元素。之所以会产生这样的结果,是因为Distinct()是通过使用默认的相等比较器对值进行比较返回序列中的非重复元素。对于值类型,默认的相等比较器是比较值是否相等,对于引用类型,默认的相等比较器是比较对象的引用地址,所以上述例子中即使属性值都相同,也不能去重。
IEqualityComparer<TSource>
聪明的我们,很容易就能发现,Linq已经为我们重载了一个去重方法,可以满足我们的需求:
public static IEnumerable<TSource> Distinct<TSource>(this IEnumerable<TSource> source, IEqualityComparer<TSource> comparer);
重载的这个方法,多提供了一个参数IEqualityComparer<TSource> comparer,是一个泛型接口,我们只需要对这个接口进行实现,即可满足我们的去重需求:
public class UserComparer : IEqualityComparer<User>
{
public bool Equals(User x, User y)
{
return x.Id == y.Id && x.Name == y.Name;
}
public int GetHashCode(User obj)
{
return obj.ToString().GetHashCode();
}
}
IEqualityComparer<TSource> 定义了两个方法,一个是Equals,一个是GetHashCode。这里我查找参考资料发现,进行比较时,默认先通过GetHashCode对两个元素进行比较,如果HashCode不同,则认为两个元素不同,如果相同则再通过Equals方法比较。所以这里我不能直接将User对象GetHashCode处理,而是先转换成了字符串再GetHashCode。通过这个重载方法,我们就可以到达目的了:
ar newList2 = list.Distinct(new UserComparer()).ToList();
甚至我们还可以实现只要某个属性相同就认为重复的效果,只需要在Equals方法按想要比较方式进行处理即可
延伸思考
Distinct的重载方法,基本已经能够满足我们的各式各样的去重需求了,但是想来想去,还是觉得有点别扭,那就是如果有类似的去重需求,我们都要新增一个类去实现IEqualityComparer<TSource>接口,不够灵活,本着封装重用的原则,想了想能否在这方面进行优化。恰巧最近在搞一个Android项目,学习了一下java,了解到java有一个匿名实现接口的语法特性,如果C#也能匿名实现接口,那就不需要增加那么多类去实现接口,会方便很多。很遗憾C#中没有这个特性,看了下资料我感觉java其实也不算是真正意义上的匿名实现,它是编译器做了手脚,编译的时候生成了一个真实的类去实现接口。在一番查找资料后,终于找到了一个很好的解决方案:
public class LambdaComparer<T> : IEqualityComparer<T>
{
private readonly Func<T, T, bool> _lambdaComparer;
private readonly Func<T, int> _lambdaHash;
public LambdaComparer(Func<T, T, bool> lambdaComparer)
: this(lambdaComparer, EqualityComparer<T>.Default.GetHashCode)
{
}
public LambdaComparer(Func<T, T, bool> lambdaComparer, Func<T, int> lambdaHash)
{
if (lambdaComparer == null)
throw new ArgumentNullException("lambdaComparer");
if (lambdaHash == null)
throw new ArgumentNullException("lambdaHash");
_lambdaComparer = lambdaComparer;
_lambdaHash = lambdaHash;
}
public bool Equals(T x, T y)
{
return _lambdaComparer(x, y);
}
public int GetHashCode(T obj)
{
return _lambdaHash(obj);
}
}
很巧妙的采用了泛型委托的方式,实现只需要定义一个类实现IEqualityComparer<TSource>接口,Equals、GetHashCode的实现,由传入的委托方法决定,接下来就简单了
var newList3 = list.Distinct(new LambdaComparer<User>((a, b) => a.Id == b.Id && a.Name == b.Name, obj => obj.ToString().GetHashCode())).ToList();
是不是很熟悉的写法,想怎么比较就怎么比较,方便快捷,不需要定义那么多类去实现接口,目的达到。Linq中有很多扩展方法,都会用到IEqualityComparer<TSource>接口。通过这种方式,可以大大提高重用率
参考资料
1、https://www.jb51.net/article/162602.htm
2、https://ask.helplib.com/c-Sharp/post_1277383
来源:https://www.cnblogs.com/cloudcmm/p/10975291.html


猜你喜欢
- 本文实例讲述了C#控制台下多线程实现方法。分享给大家供大家参考。具体如下:class Program{ static void
- 最近做了一个文件上传、下载、与在线打开文件的功能,刚开始对文件上传的界面中含有其它表单(例如输入框、密码等)在上传的过程中遇到了许多问题,下
- 结束firefox的进程,一句代码就够了,如下:Runtime.getRuntime().exec("taskkill /F /I
- 1.引入依赖 <!--mybatisplus依赖--> <dependency> &nbs
- 一、达梦数据库简介说明:有关国产数据库完整的博客太少了,所以就想弄一个完整的专栏给大家提供一些帮助。在现在这种国际形势下,网络安全是每个企业
- 本文实例讲述了C#创建windows系统用户的方法。分享给大家供大家参考。具体如下:下面的代码可以通过c#创建一个windows的本地系统账
- 本文实例讲述了Android开发中MotionEvent坐标获取方法。分享给大家供大家参考,具体如下:Android MotionEvent
- spring boot actuator介绍Spring Boot包含许多其他功能,可帮助您在将应用程序推送到生产环境时监视和管理应用程序。
- 一.介绍观察者模式(Observer Pattern)属于行为型模式。定义了对象之间的一对多依赖,让多个观察者同时监听某一个主题对象,类似于
- Android onKeyDown监听返回键无效的解决办法当我们的Activity继承了TabActivity,在该类中重写on
- 环境搭建spring boot的简介以往我们开发时用到spring总是避免不了繁琐的配置,例如我们要配置一个数据库连接,可能需要以下几步:1
- NameServer1.架构设计消息中间件的设计思路一般都是基于主题订阅与发布的机制,RocketMQ也不例外。RocketMQ中,消息生产
- 本文实例讲述了Android桌面插件App Widget用法。分享给大家供大家参考,具体如下:应用程序窗口小部件App Widgets应用程
- Android对这种方法进行了封装,我们没有权限去调用这个方法,所以我们只能通过AIDL,然后利用Java的反射机制去调用系统级的方法。下面
- 1.内容中含有xml预定好的实体,如“<”和“&”,对xml来说是禁止使用的,针对这种字符,解决方式是使用CDATA部件以&q
- 前言 这其实是一道面试题,是我在面试百度的时候被问到的,当时没有答出来(因为自己真的很菜),后来在网上寻找答案,看到也是一头雾水,
- Java空字符串与null的区别:1、类型 null表示的是一个对象的值,而并不是一个字符串。例如声明一个对象的引用,String a =
- 1. 使用方法首先从http://repo1.maven.org/maven2/com/alibaba/druid/&
- 本文实例讲述了Java继承Thread类创建线程类。分享给大家供大家参考,具体如下:一 点睛通过继承Thread类创建线程并启动多线程的步骤
- 在Java中,泛型的引入是为了在编译时提供强类型检查和支持泛型编程。为了实现泛型,Java编译器应用类型擦除实现: &