好吧,在你因为互联网上发布了数百个类似的声音问题而疯狂之前,我可以向你保证,我刚刚花了几个小时阅读所有这些问题并且没有找到我的问题的答案。
基本上,我的一个大型应用程序遇到了Binding
属性上的某些ListBox.SelectedItem
s将停止工作或者在对当前所选项目进行编辑后程序崩溃的情况。我最初问这里的'An item with the same key has already been added' Exception on selecting a ListBoxItem from code问题,但没有得到答案。
直到本周,我才有时间解决这个问题。现在简而言之,我找出了问题的原因。这是因为我的数据类型类已经覆盖了Equals
方法,因此也覆盖了GetHashCode
方法。
现在对于那些不知道这个问题的人,我发现你只能使用不可变字段/属性来实现GetHashCode
方法。使用Harvey Kwok对Overriding GetHashCode()帖子的回答来解释这个:
问题是Dictionary和HashSet集合正在使用GetHashCode将每个项目放在存储桶中。如果基于某些可变字段计算哈希码,并且在将对象放入HashSet或Dictionary后实际更改了字段,则无法再从HashSet或Dictionary中找到该对象。
所以实际问题是因为我在GetHashCode
方法中使用了可变属性。当用户在UI中更改这些属性值时,对象的关联哈希码值会发生更改,然后在其集合中无法再找到项目。
所以,我的问题是处理我需要在没有不可变字段的类中实现GetHashCode
方法的情况的最佳方法是什么?对不起,让我更具体一点,因为之前已经提出了这个问题。
Overriding GetHashCode()帖子中的答案表明,在这些情况下,最好简单地返回一个常数值...一些建议返回值1
,而其他建议返回一个素数。就个人而言,我看不出这些建议之间有任何区别,因为我认为只有一个桶用于其中任何一个。
此外,Eric Lippert博客中的Guidelines and rules for GetHashCode文章有一个标题为指南的部分:哈希码的分布必须是“随机的”,这突出了使用导致使用的桶不够的算法的缺陷。他警告说,算法会减少使用的桶数,并在桶变得非常大时导致性能问题。当然,返回常数属于这一类。
我想了一个额外的Guid
字段添加到我的所有数据类型类(只是在C#,而不是数据库)专门用于和仅在GetHashCode
方法中使用。所以我想在这个长篇介绍的最后,我的实际问题是哪个实现更好?总结一下:
当在没有不可变字段的类中重写Object.GetHashCode()时,最好从GetHashCode
方法返回一个常量,还是为每个类创建一个额外的readonly
字段,仅用于GetHashCode
方法?如果我应该添加一个新字段,它应该是什么类型,我不应该将它包含在Equals
方法中?
虽然我很高兴收到任何人的答案,但我真的希望得到高级开发人员的答案,他们对这个主题有充分的了解。
回到基础。你看了我的文章;再读一遍。与您的情况相关的两个铁定规则是:
这些是正确性的要求。如果你不能保证这两件简单的事情,那么你的程序将是不正确的。
你提出两个解决方案。
您的第一个解决方案是始终返回常量。这符合两个规则的要求,但您将在哈希表中简化为线性搜索。你也可以使用一个列表。
您建议的另一个解决方案是以某种方式为每个对象生成哈希码并将其存储在对象中。如果相等的项具有相同的哈希码,则这是完全合法的。如果您这样做,那么您受到限制,如果哈希码不同,则x等于y必须为false。这似乎使价值平等基本上不可能。因为如果你想要引用相等性,你不会首先重写Equals,这似乎是一个非常糟糕的主意,但是如果equals是一致的,它是合法的。
我提出了第三种解决方案,即:永远不要将对象放在哈希表中,因为哈希表首先是错误的数据结构。哈希表的要点是快速回答问题“这组不可变值中的给定值是什么?”并且您没有一组不可变值,因此请勿使用哈希表。使用正确的工具完成工作。使用列表,并忍受线性搜索的痛苦。
第四个解决方案是:对用于相等的可变字段进行哈希处理,在每次变异之前从所有哈希表中删除该对象,然后将其放回原处。这符合两个要求:哈希代码同意相等,哈希表中的对象哈希是稳定的,并且您仍然可以快速查找。
我要么创建一个额外的readonly
字段,要么扔NotSupportedException
。在我看来,另一种选择毫无意义。让我们看看为什么。
提供不同的哈希码很容易,例如:
class Sample
{
private static int counter;
private readonly int hashCode;
public Sample() { this.hashCode = counter++; }
public override int GetHashCode()
{
return this.hashCode;
}
public override bool Equals(object other)
{
return object.ReferenceEquals(this, other);
}
}
从技术上讲,你必须注意创造太多的物体,并在这里溢出counter
,但实际上我认为这不会成为任何人的问题。
这种方法的问题是实例永远不会比较平等。但是,如果您只想将Sample
的实例用作其他类型的集合的索引,那就完全没问题了。
如果存在不同实例应该比较的任何情况,那么乍看之下除了返回常量之外别无选择。但那会让你离开?
在容器内定位实例将始终退化为等效的线性搜索。因此,通过返回常量,您可以允许用户为您的类创建一个键控容器,但该容器将展示LinkedList<T>
的性能特征。对于熟悉你班级的人来说,这可能是显而易见的,但我个人认为这是让人们在脚下射击。如果您事先知道Dictionary
不会像人们预期的那样表现,那么为什么让用户创建一个呢?在我看来,最好扔NotSupportedException
。
有些人会不同意上述情况,当这些人比自己聪明时,人们应该注意。首先,this code analysis warning说GetHashCode
不应该抛出。这是值得考虑的事情,但我们不要教条。有时你必须打破规则是有原因的。
然而,这还不是全部。在他的blog post on the subject中,Eric Lippert说如果你从GetHashCode
内部投掷那么
由于性能原因,您的对象不能成为许多内部使用哈希表的LINQ到对象查询的结果。
失去LINQ当然是一个无赖,但幸运的是,这条路并没有结束。使用哈希表的许多(所有?)LINQ方法都有重载,它们接受在散列时使用的IEqualityComparer<T>
。所以你实际上可以使用LINQ,但它会不那么方便。
最后,您必须自己权衡选项。我的观点是,最好使用白名单策略(在需要时提供IEqualityComparer<T>
),只要它在技术上是可行的,因为这会使代码显式化:如果有人试图天真地使用该类,他们会得到一个异常,有助于告诉他们什么是继续进行,并且在使用它的任何地方都可以看到相等比较器,使得类的非凡行为立即清晰。
我想要覆盖Equals
的地方,但是对象没有合理的不可变“关键字”(无论出于何种原因,使整个对象不可变是没有意义的),在我看来,只有一个“正确”的选择:
GetHashCode
散列与Equals
使用相同的字段。 (这可能是所有领域。)(返回一个常量值会影响字典性能。抛出异常会导致太多有用的情况,其中对象被缓存但未被修改.GetHashCode
的任何其他实现都是错误的。)
无论如何,这会让用户陷入麻烦,这可能是他们的错。 (具体来说:使用不应该使用的字典,或者在上下文中使用模型类型,它们应该使用使用引用相等性的视图模型类型。)
或许我不应该首先压倒Equals
。
如果类真的不包含可以计算哈希值的常量,那么我会使用比GUID更简单的东西。只需使用在类中(或在包装类中)保留的随机数。
一种简单的方法是将hashCode存储在私有成员中,并在第一次使用时生成它。如果您的实体不经常更改,并且您不会使用两个不同的Equal(您的Equals方法返回true)的对象作为字典中的键,那么这应该没问题:
private int? _hashCode;
public override int GetHashCode() {
if (!_hashCode.HasValue)
_hashCode = Property1.GetHashCode() ^ Property2.GetHashCode() etc... based on whatever you use in your equals method
return _hashCode.Value;
}
但是,如果您有对象a和对象b,其中a.Equals(b)== true,并且您使用a作为键(词典[a] = value)在词典中存储条目。 如果a没有改变,那么dictionary [b]将返回值,但是,如果在将条目存储在字典中之后更改a,则字典[b]很可能会失败。唯一的解决方法是在任何键更改时重新发送字典。