• Linq查询中distinct 查询的局限性问题
  • Cathy 发表于 2016/3/28 12:58:00 | 分类标签: Linq查询 distinct过滤
  • 问题引出:在实际中遇到一个问题,要进行集合去重,集合内存储的是引用类型,需要根据id进行去重。这个时候linq 的distinct 就不够用了,对于引用类型,它直接比较地址。测试数据如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Person
    {
        public int ID { getset; }
        public string Name { getset; }
    }
    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 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 { getset; }
        public string Name { getset; }
     
        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去重的,所以第三种方式提供了最优雅的实现。如果是其它情况,用前面的方法更通用。

  • 请您注意

    ·自觉遵守:爱国、守法、自律、真实、文明的原则

    ·尊重网上道德,遵守《全国人大常委会关于维护互联网安全的决定》及中华人民共和国其他各项有关法律法规

    ·严禁发表危害国家安全,破坏民族团结、国家宗教政策和社会稳定,含侮辱、诽谤、教唆、淫秽等内容的作品

    ·承担一切因您的行为而直接或间接导致的民事或刑事法律责任

    ·您在编程中国社区新闻评论发表的作品,本网站有权在网站内保留、转载、引用或者删除

    ·参与本评论即表明您已经阅读并接受上述条款

  • 感谢本文作者
  • 作者头像
  • 昵称:Cathy
  • 加入时间:2013/6/11 0:00:00
  • TA的签名
  • 这家伙很懒,虾米都没写
  • +进入TA的空间
  • 以下内容也很赞哦
分享按钮