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