败给了IEqualityComparer
LINQ中的Distinct方法能够帮助我们轻松地剔除集合里面相同的元素。 它提供了2个重载函数,其中一个允许我们传入IEqualityComparer<T> 接口, 给我们充分的自由来决定2个元素是否相同。
为了实现剔除一堆string集合中相同的string,我写下了如下的代码。 注:不区分大小写
source.Distinct(new StringComparer());
internal class StringComparer : IEqualityComparer<string>
{
public bool Equals(string x, string y)
{
return string.Compare(x, y, true) == 0;
}
public int GetHashCode(string obj)
{
return obj.GetHashCode();
}
}
结果调试的时候始终不对,结果始终不能剔除那些只是大小写有区别的字符串,如"good” 跟"Good”.
当时因为时间紧张,就先提交了,事后发现原来还是自己不严谨,败给了GetHashCode()方法。
印象中自己没怎么实现过IEqualityComparer, 到实现过多次的IComparer接口。在上面的例子中虽然我意识到在比较的时候忽略大小写,但是再实现GetHashCode(), 随手地用了上面的方式。
问题就在这,既然我们认为"good” 跟"Good”是一样的,那么我们必须给他俩返回一样的hash code,但显然以我上面实现方式它俩返回的hash code是不一样的。
在鄙人的《避免陷阱,重现Equals方法您需要注意的其中2个原则》与《Just Reflect 》 中,其实我是多次提到了这个问题,想不到这次还是马失前蹄了,罪过啊。
所以正确的写法应该是:
public int GetHashCode(string obj)
{
return obj.ToUpper().GetHashCode();
}
扩充话题: 既然第一次写法返回的结果不正确,这就能说明Distinct扩展方法是先调用GetHashCode(), 再调用Equals(). 因为如果是先调用Equals()方面,理论上"good” 跟"Good” 是能剔除一个的。 问题是为什么要先调用GetHashCode(), 或则换种说法是为什么Distinct方法接受IEqualityComparer接口, 而不是IComparer接口。 因为判定2个对象是否相同,只需调用Equals() 方法就好。或者说因为 ICompare接口返回的是int,而不是直接的bool类型吗?那为啥不是IEquatable呢?
调试Framework的源代码,可以发现如下踪迹:
static IEnumerable<TSource> DistinctIterator<TSource>(IEnumerable<TSource> source, IEqualityComparer<TSource> comparer) {
Set<TSource> set = new Set<TSource>(comparer);
foreach (TSource element in source)
if (set.Add(element)) yield return element;
}
也就是说Distinct执行时在遍历集合的时候,是把那些非重复的元素放在了Set容器里,而非普通的List,这样的好处是因为Set是一个HashTable结构的容器类,可以提高执行效率。我们知道在HashTable里找一个元素的间复杂度为O(1). 而在List里查找的效率却是O(n)。
因此将Distinct方法接受IEqualityComparer接口,是很有必要的, 目前按照它的实现方式时间复杂度为O(n)。 而如果改为传IEquatable接口,那么时间复杂度就得变为O(n2).