![](/img/trans.png)
[英]Why are you expected to override GetHashCode and Equals when overloading the equality operator?
[英]Using GetHashCode to test equality in Equals override
是否可以调用 GetHashCode 作为从 Equals 覆盖内部测试相等性的方法?
例如,这个代码可以接受吗?
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;
}
}
其他人是对的; 你的平等操作被打破了。 为了显示:
public static void Main()
{
var c1 = new Class1() { A = "apahaa", B = null };
var c2 = new Class1() { A = "abacaz", B = null };
Console.WriteLine(c1.Equals(c2));
}
我想您希望该程序的输出为“假”,但根据您对相等性的定义,它在 CLR 的某些实现中为“真”。
请记住,只有大约 40 亿个可能的哈希码。 有超过 40 亿个可能的六个字母字符串,因此其中至少有两个具有相同的哈希码。 我给你们看了两个; 还有无限多的。
一般来说,您可以预期,如果有 n 个可能的哈希码,那么一旦您拥有 n 个元素的平方根,发生冲突的几率就会急剧上升。 这就是所谓的“生日悖论”。 对于我关于为什么不应该依赖哈希码进行相等性的文章,请参阅:
http://blogs.msdn.com/b/ericlippert/archive/2010/03/22/socks-birthdays-and-hash-collisions.aspx
不,这不行,因为它不是
equality <=> hashcode equality
。
这只是
equality => hashcode equality
。
或在另一个方向:
hashcode inequality => inequality
。
引用http://msdn.microsoft.com/en-us/library/system.object.gethashcode.aspx :
如果两个对象比较相等,则每个对象的 GetHashCode 方法必须返回相同的值。 但是,如果两个对象不相等,则两个对象的 GetHashCode 方法不必返回不同的值。
我会说,除非您希望Equals
基本上意味着“与您的类型具有相同的哈希码”,否则没有,因为两个字符串可能不同但共享相同的哈希码。 概率可能很小,但不是零。
不,这不是测试相等性的可接受方式。 2 个不相等的值很可能具有相同的哈希码。 这将导致您的Equals
true
在应该返回false
时返回false
你可以调用GetHashCode
确定的项目是不相等的,但如果两个对象返回相同的散列码,这并不意味着他们是平等的。 两个项目可以具有相同的哈希码,但不能相等。
如果比较两个项目的成本很高,那么您可以比较哈希码。 如果他们不平等,那么你可以保释。 否则(哈希码相等),您必须进行完整比较。
例如:
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
调用GetHashCode
的唯一时间是计算对象的哈希值(例如,因为您缓存它)比检查相等性要便宜得多。 在这种情况下,您可以说if (this.GetHashCode() != other.GetHashCode()) return false;
以便您可以快速验证对象是否不相等。
那么你什么时候会这样做呢? 我编写了一些代码,它定期截取屏幕截图,并尝试找出屏幕更改后的时间。 由于我的屏幕截图大小为 8MB,并且在屏幕截图间隔内变化的像素相对较少,因此搜索它们的列表以查找相同的像素是相当昂贵的。 哈希值很小,每个屏幕截图只需计算一次,可以轻松消除已知的不相等值。 事实上,在我的应用程序中,我认为具有相同的哈希值足够接近相等,以至于我什至没有费心实现Equals
重载,导致 C# 编译器警告我我正在重载GetHashCode
而不重载Equals
。
这是错误的实现,正如其他人所说的那样。
您应该使用GetHashCode
短路相等性检查,例如:
if (other.GetHashCode() != this.GetHashCode() return false;
在Equals
方法中,只有当您确定随后的 Equals 实现比GetHashCode
昂贵得多时,这不是绝大多数情况。
在您展示的这个实现中(占 99% 的情况)它不仅坏了,而且速度也慢得多。 原因是什么? 计算属性的散列几乎肯定会比比较它们慢,因此您甚至没有在性能方面获得收益。 实现正确的GetHashCode
的优点是当您的类可以作为哈希表的键类型时,哈希只计算一次(并且该值用于比较)。 在您的情况下,如果GetHashCode
在一个集合中,它将被多次调用。 尽管GetHashCode
本身应该很快,但它并不比等效的Equals
快。
要进行基准测试,请在此处运行您的Equals
(一个正确的实现,取出当前基于哈希的实现)和GetHashCode
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(1) 复杂度转向 O(n) 复杂度。 这样做的风险与可用槽的数量成反比,因此在达到一定大小后我们调整哈希表的大小(即使一切都很理想,如果存储的项目数量大于数量,我们最终必须这样做插槽)。
在调整大小时重新插入项目显然取决于哈希码。 正因为如此,虽然在对象中记住GetHashCode()
很少有意义(它只是在大多数对象上调用得不够频繁),但在哈希表本身中记住它肯定是有意义的(或者也许,记住生成的结果,例如如果您使用 Wang/Jenkins 哈希重新哈希以减少由错误的GetHashCode()
实现造成的损害。
现在,当我们开始插入时,我们的逻辑将是这样的:
因此,在这种情况下,我们必须在比较相等性之前获取哈希码。 我们也已经预先计算了现有对象的哈希码以允许调整大小。 这两个事实的结合意味着对第 4 项进行比较是有意义的:
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 规范化相等比较(因此它甚至不能缩短长度),那么节省很值得。
通常记住哈希码没有意义,因为它们的使用频率不够高,不足以提高性能,但将它们存储在哈希集或哈希表本身中是有意义的。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.