程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> linq distinct 不夠用了!,linqdistinct

linq distinct 不夠用了!,linqdistinct

編輯:C#入門知識

linq distinct 不夠用了!,linqdistinct


問題引出:在實際中遇到一個問題,要進行集合去重,集合內存儲的是引用類型,需要根據id進行去重。這個時候linq 的distinct 就不夠用了,對於引用類型,它直接比較地址。測試數據如下:

    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先分一下組,再取第一個數據即可。例如:

list.GroupBy(x => x.ID).Select(x => x.FirstOrDefault()).ToList()

通常通過GroupBy去實現也是可以的,畢竟在內存操作還是很快的。但這裡我們用別的方式去實現,並且找到最好的實現方式。

 

一、通過IEqualityComparer接口

IEnumerable<T> 的擴展方法 Distinct 定義如下:

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> 的重載。該接口的定義如下:

// 類型參數 T: 要比較的對象的類型。
public interface IEqualityComparer<T>
{
    bool Equals(T x, T y);
    int GetHashCode(T obj);
}

通過實現這個接口我們就可以實現自己的比較器,定義自己的比較規則了。

這裡有一個問題,IEqualityComparer<T> 的 T 是要比較的對象的類型,在這裡就是 Person,那這裡如何去獲得 Person 的屬性 id呢?或者說,對於任何類型,我如何知道要比較的是哪個屬性?答案就是:委托。通過委托,要比較什麼屬性由外部指定。這也是linq 擴展方法的設計,參數都是委托類型的,也就是規則由外部定義,內部只負責調用。ok,我們看最後實現的代碼:

    //通過繼承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)之前沒有把擴展方法貼出來,而且看到有朋友提到比較字符串忽略大小寫的問題(其實上面有兩個構造函數就可以解決這個問題)。這裡擴展方法可以寫為:

    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忽略大小寫比較,就可以寫成:

list.Distinct(x => x.Name,StringComparer.CurrentCultureIgnoreCase).ToList(); //StringComparer實現了IEqualityComaparer<string> 接口

 

二、通過哈希表。第一種做法的缺點是不僅要定義新的擴展方法,還要定義一個新類。能不能只有一個擴展方法就搞定?可以,通過Dictionary就可以搞定(有HashSet就用HashSet)。實現方式如下:

        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時,是會進入到這兩個方法的。代碼如下:

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去重的,所以第三種方式提供了最優雅的實現。如果是其它情況,用前面的方法更通用。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved