Using GetHashCode to test equality in Equals override
是否可以调用GetHashCode作为方法从Equals重写内部测试相等性?
例如,这个代码可以接受吗?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | public class Class1 { public string A { get; set; } public string B { get; set; } public override bool Equals(object obj) { Class1 other = obj as Class1; return other != null && other.GetHashCode() == this.GetHashCode(); } public override int GetHashCode() { int result = 0; result = (result ^ 397) ^ (A == null ? 0 : A.GetHashCode()); result = (result ^ 397) ^ (B == null ? 0 : B.GetHashCode()); return result; } } |
其他人是对的,你的平等行动被打破了。举例说明:
1 2 3 4 5 6 |
我想您希望该程序的输出是"假",但是根据您对相等的定义,在某些CLR实现上是"真"。
记住,只有大约40亿个可能的散列码。有超过40亿个可能的六个字母字符串,因此其中至少有两个具有相同的哈希代码。我给你们看了两个,还有无限多的。
一般来说,如果有n个可能的散列码,那么当你使用n个元素的平方根时,发生冲突的几率会显著增加。这就是所谓的"生日悖论"。有关为何不应依赖哈希代码实现平等的文章,请参阅:
http://blogs.msdn.com/b/ericlippet/archive/2010/03/22/socks-birthdays-and-hash-collisions.aspx
不,不好,因为不好
只是
或者在另一个方向:
引用http://msdn.microsoft.com/en-us/library/system.object.gethashcode.aspx:
If two objects compare as equal, the GetHashCode method for each object must return the same value. However, if two objects do not compare as equal, the GetHashCode methods for the two object do not have to return different values.
< /块引用>
我想说,除非你想让
Equals 基本上意味着你的类型"有相同的散列码",否则不,因为两个字符串可能不同,但共享相同的散列码。概率可能很小,但不是零。不能这样说,因为散列码相等,所以对象必须相等。
在
Equals 的内部调用GetHashCode 的唯一时间是,计算对象的散列值(例如,因为缓存它)比检查是否相等便宜得多。在这种情况下,您可以说if (this.GetHashCode() != other.GetHashCode()) return false; ,这样您就可以快速验证对象是否相等。你什么时候会这样做?我写了一些代码,定期截屏,并试图找出屏幕更改后的时间。因为我的截图是8MB,并且在截图间隔内变化的像素相对较少,所以搜索它们的列表以查找相同的像素是相当昂贵的。散列值很小,每个截图只需要计算一次,这样就很容易消除已知的不相等值。事实上,在我的应用程序中,我决定使用相同的散列足够接近相等,以至于我甚至不需要执行
Equals 重载,这导致C编译器警告我正在重载GetHashCode ,而不重载Equals 。您可以调用
GetHashCode 来确定项是否不相等,但是如果两个对象返回相同的哈希代码,这并不意味着它们是相等的。两个项目可以具有相同的哈希代码,但不能相等。如果比较两个项目很昂贵,那么您可以比较哈希代码。如果他们不平等,你可以保释。否则(散列码是相等的),您必须进行完整的比较。
例如:
1
2
3
4
5
6
7 public override bool Equals(object obj)
{
Class1 other = obj as Class1;
if (other == null || other.GetHashCode() != this.GetHashCode())
return false;
// the hash codes are the same so you have to do a full object compare.
}不,这不是一个可以接受的平等测试方法。两个不相等的值很可能具有相同的哈希代码。这将导致您执行
Equals 返回true ,当它应该返回false 时。
正如其他人所说,这是错误的实现。
您应该使用
GetHashCode 之类的工具来短路相等性检查:
1
2 if (other.GetHashCode() != this.GetHashCode()
return false;在
Equals 方法中,只有当您确定随后的equals实现比GetHashCode 要昂贵得多,后者并不是绝大多数情况。在这一个实现中,您已经展示了(99%的情况)它不仅中断了,而且速度慢得多。原因是什么?计算属性的散列值几乎肯定要比比较它们慢,因此在性能方面您甚至无法获得。实现适当的
GetHashCode 的好处在于,类可以是哈希表的键类型,其中哈希只计算一次(该值用于比较)。在您的情况下,如果GetHashCode 在集合中,它将被多次调用。虽然GetHashCode 本身应该很快,但它的速度并不比等效的Equals 快。要进行基准测试,请在这里运行您的
Equals (一个适当的实现,去掉当前基于哈希的实现)和GetHashCode 。
1
2
3
4
5
6
7 var watch = Stopwatch.StartNew();
for (int i = 0; i < 100000; i++)
{
action(); //Equals and GetHashCode called here to test for performance.
}
watch.Stop();
Console.WriteLine(watch.Elapsed.TotalMilliseconds);有一种情况是,使用哈希代码作为等式比较的捷径是有意义的。
考虑一下您正在构建哈希表或哈希集的情况。实际上,我们只考虑哈希集(哈希表通过同时保存一个值来扩展它,但这并不相关)。
我们可以采用不同的方法,但在所有方法中,您都有少量的槽,散列值可以放在其中,我们采用开放式或封闭式方法(这只是为了好玩,有些人对其他人使用相反的行话);如果我们在同一槽中碰撞两个不同的对象,我们可以将它们存储在同一个插槽(但有一个链接列表或用于实际存储对象的列表)或通过重新探测来选择不同的插槽(有不同的策略)。
现在,无论采用哪种方法,我们都将不再使用哈希表,而是使用O(n)复杂性。这样做的风险与可用插槽的数量成反比,因此在确定大小后,我们会调整哈希表的大小(即使一切都是理想的,如果存储的项目数量大于插槽的数量,我们最终也必须这样做)。
重新插入调整大小的项显然取决于哈希代码。因此,虽然在一个对象中Memoise
GetHashCode() 很少有意义(它在大多数对象中的调用频率不够),但在哈希表本身中Memoise(或者可能,Memoise是一个产生的结果,例如使用Wang/Jenkins哈希对其进行哈希处理以减少由坏的GetHashCode() 造成的损坏)确实有意义。实施)。现在,当我们开始插入逻辑时,我们的逻辑是这样的:
获取对象的哈希代码。 获取对象的槽。 如果插槽是空的,请将对象放入其中并返回。 如果slot包含相等的对象,那么我们就完成了哈希集的创建,并且可以替换哈希表的值。这样做,然后返回。 根据冲突策略尝试下一个槽,然后返回到第3项(如果我们循环的次数太多,可能会调整大小)。 因此,在这种情况下,我们必须在比较相等性之前获取哈希代码。我们还预先计算了现有对象的哈希代码,以允许调整大小。这两个事实的结合意味着我们对项目4进行比较是有意义的,因为:
1
2
3
4
5
6
7
8
9
10 private bool IsMatch(KeyType newItem, KeyType storedItem, int newHash, int oldHash)
{
return ReferenceEquals(newItem, storedItem) // fast, false negatives, no false positives (only applicable to reference types)
||
(
newHash == oldHash // fast, false positives, no fast negatives
&&
_cmp.Equals(newItem, storedItem) // slow for some types, but always correct result.
);
}显然,这一优势取决于
_cmp.Equals 的复杂性。如果我们的密钥类型是int ,那么这将是完全浪费。如果我们的键类型where string和我们使用的是不区分大小写的Unicode标准化相等比较(这样它甚至不能在长度上进行快捷方式),那么保存就很值得了。通常,记忆散列代码是没有意义的,因为它们的使用频率不足以赢得性能,但是将它们存储在散列集或散列表本身中是有意义的。