关于GetHashCode的重要性
其他人已经评论过任何自定义IEqualityComparer<T> 实现应该真正包含GetHashCode 方法;但是没有人会费心去详细解释为什么。
原因如下。您的问题特别提到了 LINQ 扩展方法;几乎所有这些都依赖哈希码才能正常工作,因为它们在内部利用哈希表来提高效率。
以Distinct 为例。如果它使用的只是Equals 方法,请考虑此扩展方法的含义。如果您只有Equals,如何确定某个项目是否已按顺序扫描?您枚举您已经查看过的整个值集合并检查匹配项。这将导致Distinct 使用最坏情况的 O(N2) 算法而不是 O(N) 算法!
幸运的是,情况并非如此。 Distinct 不只是使用Equals;它也使用GetHashCode。事实上,如果没有提供正确 GetHashCode 的 IEqualityComparer<T>,它绝对不会正常工作。下面是一个人为的例子来说明这一点。
假设我有以下类型:
class Value
{
public string Name { get; private set; }
public int Number { get; private set; }
public Value(string name, int number)
{
Name = name;
Number = number;
}
public override string ToString()
{
return string.Format("{0}: {1}", Name, Number);
}
}
现在假设我有一个List<Value>,我想找到所有具有不同名称的元素。这是Distinct 使用自定义相等比较器的完美用例。所以让我们使用来自Aku's answer 的Comparer<T> 类:
var comparer = new Comparer<Value>((x, y) => x.Name == y.Name);
现在,如果我们有一堆具有相同Name 属性的Value 元素,它们应该都折叠成Distinct 返回的一个值,对吧?让我们看看...
var values = new List<Value>();
var random = new Random();
for (int i = 0; i < 10; ++i)
{
values.Add("x", random.Next());
}
var distinct = values.Distinct(comparer);
foreach (Value x in distinct)
{
Console.WriteLine(x);
}
输出:
x: 1346013431
x: 1388845717
x: 1576754134
x: 1104067189
x: 1144789201
x: 1862076501
x: 1573781440
x: 646797592
x: 655632802
x: 1206819377
嗯,那没用,是吗?
GroupBy 呢?让我们试试吧:
var grouped = values.GroupBy(x => x, comparer);
foreach (IGrouping<Value> g in grouped)
{
Console.WriteLine("[KEY: '{0}']", g);
foreach (Value x in g)
{
Console.WriteLine(x);
}
}
输出:
[键='x:1346013431']
x: 1346013431
[键='x:1388845717']
x: 1388845717
[键='x:1576754134']
x: 1576754134
[键='x:1104067189']
x: 1104067189
[键='x:1144789201']
x: 1144789201
[键='x:1862076501']
x: 1862076501
[键='x:1573781440']
x: 1573781440
[键='x:646797592']
x: 646797592
[键='x:655632802']
x: 655632802
[键='x:1206819377']
x: 1206819377
再次:没用。
如果您考虑一下,Distinct 在内部使用 HashSet<T>(或等效)是有意义的,GroupBy 在内部使用类似 Dictionary<TKey, List<T>> 的东西是有意义的。这可以解释为什么这些方法不起作用吗?让我们试试这个:
var uniqueValues = new HashSet<Value>(values, comparer);
foreach (Value x in uniqueValues)
{
Console.WriteLine(x);
}
输出:
x: 1346013431
x: 1388845717
x: 1576754134
x: 1104067189
x: 1144789201
x: 1862076501
x: 1573781440
x: 646797592
x: 655632802
x: 1206819377
是的...开始有意义了?
希望从这些示例中可以清楚为什么在任何 IEqualityComparer<T> 实现中包含适当的 GetHashCode 如此重要。
原答案
扩展至orip's answer:
这里可以进行一些改进。
- 首先,我会使用
Func<T, TKey> 而不是Func<T, object>;这将防止在实际 keyExtractor 本身中对值类型键进行装箱。
- 其次,我实际上要添加一个
where TKey : IEquatable<TKey> 约束;这将防止在 Equals 调用中装箱(object.Equals 采用 object 参数;您需要 IEquatable<TKey> 实现来采用 TKey 参数而不装箱)。显然,这可能会造成过于严格的限制,因此您可以创建一个没有约束的基类和一个有它的派生类。
生成的代码如下所示:
public class KeyEqualityComparer<T, TKey> : IEqualityComparer<T>
{
protected readonly Func<T, TKey> keyExtractor;
public KeyEqualityComparer(Func<T, TKey> keyExtractor)
{
this.keyExtractor = keyExtractor;
}
public virtual bool Equals(T x, T y)
{
return this.keyExtractor(x).Equals(this.keyExtractor(y));
}
public int GetHashCode(T obj)
{
return this.keyExtractor(obj).GetHashCode();
}
}
public class StrictKeyEqualityComparer<T, TKey> : KeyEqualityComparer<T, TKey>
where TKey : IEquatable<TKey>
{
public StrictKeyEqualityComparer(Func<T, TKey> keyExtractor)
: base(keyExtractor)
{ }
public override bool Equals(T x, T y)
{
// This will use the overload that accepts a TKey parameter
// instead of an object parameter.
return this.keyExtractor(x).Equals(this.keyExtractor(y));
}
}