使⽤Distinct()内置⽅法对List集合的去重问题
说到对集合去重处理,第⼀时间想到的肯定是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>
{equals()方法
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>接⼝。通过这种⽅式,可以⼤⼤提⾼重⽤率
参考资料
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论