使用Distinct()内置方法对List集合的去重 问题

摘要:
说到集的重复数据消除,我首先想到的是Linq的Distinct扩展方法。很容易消除常规值类型集的重复。它是一个直接列表Distinct()。产生此结果的原因是Distinct()通过使用默认的相等比较器比较值来返回序列中的非重复元素。

说到对集合去重处理,第一时间想到的肯定是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.cnblogs.com/mirageJ/p/8950842.html

2、https://ask.helplib.com/c-Sharp/post_1277383

免责声明:文章转载自《使用Distinct()内置方法对List集合的去重 问题》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇java 单例模式resin安装下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

算法竞赛专题解析(5):简单数据结构

本系列是这本算法教材的扩展资料:《算法竞赛入门到进阶》(京东当当 ) 清华大学出版社如有建议,请联系:(1)QQ 群,567554289;(2)作者QQ,15512356 目录 1 链表 1.1 动态链表 1.2 用结构体实现单向静态链表 1.3 用结构体实现双向静态链表 1.4 用一维数组实现单向静态链表 1.5 STL list 1.6 链表习题...

kafka时间轮的原理(一)

概述早就想写关于kafka时间轮的随笔了,奈何时间不够,技术感觉理解不到位,现在把我之前学习到的进行整理一下,以便于以后并不会忘却。kafka时间轮是一个时间延时调度的工具,学习它可以掌握更加灵活先进的定时器技术,补益多多。本文由浅到深进行讲解,先讲解定时器基础以及常用定时器,接着就是主要的kafka时间轮实现。大部分都是原理。后期作者写第二部分的时候专门...

redis的五种存储类型的具体用法

String 类型操作 string是redis最基本的类型,而且string类型是二进制安全的。意思是redis的string可以包含任何数据。比如jpg图片或者序列化的对象 $redis->set('key','TK'); $redis->set('number','1'); $redis->setex('key',5,'TK')...

Android智能指针sp wp详解

研究Android的时候,经常会遇到sp、wp的东西,网上一搜,原来是android封装了c++中对象回收机制。说明:1. 如果一个类想使用智能指针,那么必须满足下面两个条件:a. 该类是虚基类RefBase的子类或间接子类b. 该类必须定义虚构造函数。如virtual ~MyClass(); 2. 本文以类BBinder来进行说明,其余类使用sp或wp...

《深入实践C++模板编程》之二——模板类

1、类的模板的使用 类,由于没有参数,所以没有模板实参推导机制。 1 #include <stdexcept> 2 3 template<typename T> class my_stack; 4 5 template<typename T> 6 class list_node 7 {...

c# lambda distinct

在写程序的时候会遇见这样的问题,那就是去重,有什么方法更快呢。当去重时,首先想到的是自己写代码,代码大概如下: private static void distinctListIntTest() { Console.WriteLine("未去重"); List<int> li...