問題引出:在實際中遇到一個問題,要進行集合去重,集合內存儲的是引用類型,需要根據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去重的,所以第三種方式提供了最優雅的實現。如果是其它情況,用前面的方法更通用。