给出了以下类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public class Foo
{
public int FooId { get; set; }
public string FooName { get; set; }
public override bool Equals(object obj)
{
Foo fooItem = obj as Foo;
return fooItem.FooId == this.FooId;
}
public override int GetHashCode()
{
// Which is preferred?
return base.GetHashCode();
//return this.FooId.GetHashCode();
}
} |
我已经重写了Equals方法,因为Foo表示Foo表的行。哪个是覆盖GetHashCode的首选方法?
为什么覆盖GetHashCode很重要?
- 由于冲突,特别是在使用字典时,实现equals和gethashcode非常重要。如果两个对象返回相同的hashcode,它们将通过链接插入字典中。访问item equals方法时使用。
是的,重要的是,如果您的项目将用作字典中的键或HashSet等,因为这是用来(在没有自定义IEqualityComparer的情况下)将项目分组到bucket中的。如果两个项目的哈希代码不匹配,则它们可能永远不会被认为是相等的(Equals将永远不会被调用)。
GetHashCode()方法应反映Equals逻辑,规则如下:
- 如果两个事物相等(Equals(...) == true,则它们必须返回GetHashCode()的相同值。
- 如果GetHashCode()相等,它们就不必相同;这是一次碰撞,将调用Equals来查看它是否真正相等。
在这种情况下,"return FooId;似乎是一个合适的GetHashCode()实现。如果要测试多个属性,通常使用下面这样的代码组合它们,以减少对角线冲突(即,使new Foo(3,5)与new Foo(5,3)具有不同的哈希代码):
1 2 3 4 5 6 7 8
| unchecked // only needed if you're compiling with arithmetic checks enabled
{ // (the default compiler behaviour is *disabled*, so most folks won't need this)
int hash = 13;
hash = (hash * 7) + field1 .GetHashCode();
hash = (hash * 7) + field2 .GetHashCode();
...
return hash ;
} |
为了方便起见,您也可以考虑在覆盖Equals和GetHashCode时提供==和!=操作符。
当你犯了这个错误时会发生什么的演示就在这里。
- 我能问一个问题吗?你是在乘以这些因素吗?
- 实际上,我可能会失去其中一个;重点是尽量减少碰撞的次数,这样一个对象1,0,0的散列值与0,1,0和0,0,1的散列值不同(如果你明白我的意思的话)。
- 我调整了数字使其更清晰(并添加了一个种子)。有些代码使用不同的数字-例如C编译器(匿名类型)使用0x51ed270b的种子和-1521134295的因子。
- @Leandro L&243;Pez:通常选择系数作为素数,因为它会使碰撞次数变小。
- "为了方便起见,您还可以考虑提供==和!=重写equals和gethashcode时的运算符。":Microsoft不建议为不可变的对象实现operator==msdn.microsoft.com/en-us/library/ms173147.aspx-"在不可变的类型中重写operator==
- @由于这个原因,对于不可变类型,使用gethashcode也不是一个好主意,这意味着不要使用equals!所以:如果它不是价值型的(不管class对class),不要搞平等——一个合理的经验法则。
- 所以,故意让gethashcode的精度低于等于,从而加速字典查找是否有意义?到目前为止,我一直在镜像.equals和gethashcode中使用的逻辑。编辑:回答我的问题:stackoverflow.com/questions/6305324/…
- 微软说-gethashcode方法的默认实现不保证不同对象的唯一返回值。此外,.NET框架不保证GetHashCode方法的默认实现,它返回的值在不同版本的.NET框架之间是相同的。因此,不能将此方法的默认实现用作哈希目的的唯一对象标识符。
- @一般来说,GetHashCode()在AppDomain之间从未得到保证,因此这不是一个重要的陈述;其余只是对答案中第二个项目的重新陈述。默认实现是为散列找到的,但不能用作唯一标识符——我们已经说过的所有内容。
- 所以,如果我没有从GetHashCode()得到唯一性,我们应该在任何情况下使用它吗?
- 别忘了检查field1、field2等是否…如果可能为空,则为空。
- 小说明-如果启用了/checked project compiler设置,计算哈希的这种特殊实现容易产生整数溢出异常。考虑使用未选中的包装计算
- @Neil Fair Observation;未选中是编译器默认值,但您是对的:不应该假定
- 计算哈希时,如果可以为空,我们不应该检查field1和field2是否为空吗?
- @李哥,是的,你应该
- 如果不是读到hash += (hash * 7)…?
- @理查德,不,不应该
- 有人能评论一下比尔·瓦格纳在《有效的C 4.0》中概述的以下gethashcode()规则吗?规则1)建议obja.equals(objb)==true并不意味着两个对象必须返回相同的哈希代码。这是不正确的,因为一些LINQ方法(即distinct())开始出现错误行为。我很惊讶地在这样的口径手册中发现了这样的错误:"如果两个对象相等(由operator==定义),它们必须生成相同的哈希值。否则,不能使用哈希代码在容器中查找对象。"
- @grzegorz_p是向后的;具有相同哈希代码的两个事物并不意味着它们是相等的(Equals==true),但是,报告为相等的两个事物(Equals==true)必须返回相同的哈希代码,否则它们会破坏API。你确定你没有简单地误读这段话吗?从msdn引用:"如果在测试两个对象是否相等时重写的equals方法返回true,则重写的gethashcode方法必须为两个对象返回相同的值。"
- 谢谢-但是如果要在列表中而不是字典中使用该项怎么办-在这种情况下是否仍然需要重写gethashcode?
- @bkspugeon在这种情况下不会使用它,但是在不重写gethashcode的情况下重写equals是一个非常糟糕的主意-您只是在制造一堆混乱,以便稍后将某人抓出来。
- 它只在hashmap中使用。
- @我已经读过你链接的文章,但它没有解释为什么可变类不应该使用它的==操作符重写。你或其他人能解释一下为什么会这样吗?
- 如果翻转field1和field2,此答案中的示例代码将生成相同的哈希代码。加法是交换的…
- @用户45623不可以-int field1 = 1, field2 = 2; // 646-int field1 = 2, field2 = 1; // 652;另外,碰撞是不方便的,但不是无效的。
- @马尔格拉夫德!我在心里用两行的13 * 7替换了hash * 7,但hash在第二行不是"13"。_<
实际上很难正确地实现GetHashCode(),因为除了前面提到的规则外,哈希代码在对象的生命周期中不应该改变。因此,用于计算哈希代码的字段必须是不可变的。
当我和NHibernate一起工作时,我终于找到了解决这个问题的方法。我的方法是根据对象的ID计算散列代码。只能通过构造函数设置ID,因此,如果要更改ID(这是不太可能的),则必须创建一个新对象,该对象具有新的ID,因此需要创建一个新的哈希代码。这种方法最适合于guid,因为您可以提供一个无参数的构造函数,它随机生成一个ID。
- 我认为实现has代码并不难。考虑到这些规则并在有效的C_book中得到更好的解释,我认为重写gethashcode相当容易。
- 您能否详细说明"哈希代码在对象的生命周期中不应该更改"?这是氨气特异性的吗?
- @万杰。我相信这与:如果您将对象添加到字典,然后更改对象的ID,那么在以后提取时,您将使用其他哈希来检索它,这样就永远无法从字典中获取它。
- @Darthvander,重写GetHashCode()并不困难。而且,使用引用等效(或者上面的albic的guid命题)重写equals()并不困难。使用值等效来重写equals()是很困难的,但仍然遵循以下规则:两个equals()等效的对象必须返回相同的哈希代码:哈希代码算法中只应使用不可变的对象状态。
- 微软关于gethashcode()函数的文档既没有说明也没有暗示对象哈希必须在其生命周期内保持一致。事实上,它专门解释了一种可能不允许的情况:"对象的getHashCode方法必须始终返回相同的哈希代码,只要不修改确定对象的equals方法返回值的对象状态。"
- "peterallenwebb:设计公司多方面的。NET和Java,以及一些错误。因为Java是收集的设计允许用户指定需要的替代方法和比较哈希函数,唯一的方式是把对象作为一个集合匹配对方什么。我们报告为"平等"的时候,他们是否实际上是等效的。这个定义不同类型的LED"equals"到的东西不同的均值的实践者,这使得它非常小的对象或其他对象的集合。encapsulate通用类型(要知道无论他们是"平等"。
- "哈希代码在不改变对象的寿命,在"那不是真的。
- 它是一个更好的方式说"哈希代码(不应该改变的评价of equals)在周期的对象是作为一个收集的关键",所以如果你添加对象到一个字典作为一个密钥,你必须确保他们不会重写equals和一个给定的输入输出的变化,直到你删除对象从字典。
- 我想你忘了scottchamberlain困境"在您的评论,它应该是:"哈希代码(只有在评价of equals)在不改变周期的对象是作为一个收集的关键"。右吗?
- stanislavprokop"是,你是正确的。
- 除了这个有漏洞的更新:如果在级联是指定的,我不能改变它的主键,主键是不是不可变的意味。
- "我认为scottchamberlain stanprokop的评论是正确的,但为了避免混乱,真的应该读"哈希代码(和评价of equals),在不改变周期的对象是用来作为一个密钥,一个收集"
通过重写equals,您基本上是在声明您是更了解如何比较给定类型的两个实例的人,因此您可能是提供最佳哈希代码的最佳候选。
以下是Resharper如何为您编写getHashCode()函数的示例:
1 2 3 4 5 6 7 8 9 10 11 12
| public override int GetHashCode ()
{
unchecked
{
var result = 0;
result = (result * 397) ^ m_someVar1 ;
result = (result * 397) ^ m_someVar2 ;
result = (result * 397) ^ m_someVar3 ;
result = (result * 397) ^ m_someVar4 ;
return result ;
}
} |
正如您所看到的,它只是根据类中的所有字段来猜测一个好的哈希代码,但是由于您知道对象的域或值范围,所以仍然可以提供一个更好的哈希代码。
- 这不总是返回零吗?可能应该将结果初始化为1!还需要更多的分号。
- 您知道XOR运算符(^)的作用吗?
- 正如我所说,这就是R为你写的(至少是2008年写的)。显然,这个代码片段是由程序员以某种方式调整的。至于失踪的分号…是的,看起来在我复制粘贴Visual Studio中区域选择的代码时,我把它们遗漏了。我也认为人们会同时解决这两个问题。
- @我在缺失的分号中添加了sammackrill。
- @不,它不会总是返回0。0 ^ a = a,所以0 ^ m_someVar1 = m_someVar1。他还可以将EDOCX1的初始值(8)设置为m_someVar1。
覆盖Equals()时,请不要忘记对照null检查obj参数。并比较类型。
1 2 3 4 5 6 7 8 9
| public override bool Equals(object obj)
{
if (obj == null || GetType() != obj.GetType())
return false;
Foo fooItem = obj as Foo;
return fooItem.FooId == this.FooId;
} |
原因是:与null相比,Equals必须返回false。另请参阅http://msdn.microsoft.com/en-us/library/bsc2ak47.aspx
- 在子类引用超类equals方法作为自身比较的一部分(即base.equals(obj))时,对类型的检查将失败-应改为使用
- @sweetfa:这取决于如何实现子类的equals方法。它还可以调用base.equals((basetype)obj)),这会很好地工作。
- 不,它不会:msdn.microsoft.com/en-us/library/system.object.gettype.aspx。此外,方法的实现不应该失败或成功,这取决于方法的调用方式。如果对象的运行时类型是某个baseclass的子类,那么如果obj确实等于this,则不管如何调用baseclass的equals(),baseclass的equals()都应返回true。
- 将fooItem移到顶部,然后检查它是否为空,在为空或错误类型的情况下,效果会更好。
- @除非您使用的是值类型。但为什么要重写值类型的equals。
- @是的,那么,obj as Foo是无效的。
怎么样:
1 2 3 4
| public override int GetHashCode()
{
return string.Format("{0}_{1}_{2}", prop1, prop2, prop3).GetHashCode();
} |
Assuming performance is not an issue :)
- 如果你管理一个int返回的字符串为基础的方法_ 0;
- 不,他是从字符串的GetHashCode()调用的对象,它返回int。
- 我期待这是一个几乎是我想的,不只是在玩拳击有研究价值的类型,但也string.Format性能。我是一个geeky new { prop1, prop2, prop3 }.GetHashCode()湖泊。我无法评论这一命题会将两点之间。不滥用工具。
- 这将返回true for { prop1="_X", prop2="Y", prop3="Z" }和{ prop1="", prop2="X_Y", prop3="Z_" }。你可能不希望。
- 是的,你可以总是替换下划线的符号和一些不太普通的(例如?▲,?,?,????)和你的用户不希望使用的主要符号……:)
- "这是可能的voetsjoeba只有当所有变量是字符串,但它是一个非常好的点:+ 1)
- "你有一个voetsjoeba排版:"_X_Y_Z" !="_X_Y_Z_"(额外的下划线的为第二prop3尾随点,但采取了:))
- "安全ludmiltinkov prop1/2/3.GetHashCode()变更将被调用,然后不会有所作为
只需添加以上答案:
如果不重写Equals,则默认行为是比较对象的引用。同样适用于hashcode——默认实现通常基于引用的内存地址。因为您确实重写了equals,它意味着正确的行为是比较您在equals上实现的内容,而不是引用,所以您应该对哈希代码进行同样的操作。
类的客户机将期望哈希代码具有与equals方法相似的逻辑,例如,使用IEqualityComparer的Linq方法首先比较哈希代码,并且只有当它们相等时,才会比较equals()方法(运行起来可能更昂贵),如果我们不实现哈希代码,equal对象可能具有不同的哈希码(因为它们有不同的内存地址),将被错误地确定为不相等(equals()甚至不会命中)。
此外,除了在字典中使用对象时可能找不到的问题(因为它是由一个哈希代码插入的,当您查找对象时,默认的哈希代码可能会有所不同,并且会再次调用equals(),就像Marc Gravell在他的回答中解释的那样,您还引入了对不允许相同键的字典或哈希集概念-您已经声明,当override equals时,这些对象本质上是相同的,因此您不希望它们都作为数据结构上的不同键(假定具有唯一键)。但由于它们具有不同的哈希代码,"相同"键将作为不同的键插入。
我们有两个问题要处理。
如果在对象可以更改。通常情况下,对象不会在依赖于GetHashCode()的集合。所以成本实施GetHashCode()通常不值得,或者不值得可能的。
如果有人将对象放入调用你已经凌驾于Equals()之上,而没有GetHashCode()行为端正,该人可能会花费数天时间。跟踪问题。
因此,默认情况下我会这样做。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class Foo
{
public int FooId { get; set; }
public string FooName { get; set; }
public override bool Equals (object obj )
{
Foo fooItem = obj as Foo ;
return fooItem .FooId == this.FooId;
}
public override int GetHashCode ()
{
// Some comment to explain if there is a real problem with providing GetHashCode()
// or if I just don't see a need for it for the given class
throw new Exception ("Sorry I don't know what GetHashCode should do for this class");
}
} |
- 从GetHashCode引发异常违反了对象约定。定义GetHashCode函数没有困难,任何两个相等的对象都返回相同的哈希代码;return 24601;和return 8675309;都是GetHashCode的有效实现。Dictionary的性能只有在项目数量少的情况下才是好的,项目数量多的情况下性能会很差,但在任何情况下都能正常工作。
- @supercat,如果对象中的标识字段可以更改,则不可能以合理的方式实现gethashcode,因为哈希代码决不能更改。做你所说的可能会导致一些人不得不花很多天的时间来跟踪性能问题,然后花很多周的时间在一个大型系统上重新设计以消除对字典的使用。
- 我曾经为我定义的所有需要equals()的类做过类似的事情,并且在那里我完全确信我不会将该对象用作集合中的键。然后有一天,一个程序崩溃了,在该程序中,我使用一个类似这样的对象作为devexpress xtragrid控件的输入。事实证明,Xtragrid,在我背后,是基于我的对象创建一个哈希表或其他东西。我和DevExpress的支持人员就此事进行了一次小小的争论。我说,他们将组件的功能和可靠性建立在一个未知的客户实现的模糊方法上是不明智的。
- devexpress的人相当难缠,基本上说我必须是个白痴才能在gethashcode()方法中抛出异常。我仍然认为他们应该找到另一种方法来做他们正在做的事情——我记得MarcGravell在另一条线索上描述了他如何在不依赖gethashcode()的情况下建立一个任意对象的字典——但他不记得他是如何做到的。
- @Renniepet,最好是因为抛出异常而崩溃,然后因为一个无效的实现而很难找到bug。
- 是的,这也是我的态度,直到DevExpress Xtragrid问题。现在,我故意忽略了编写gethashcode()方法,并禁止来自代码分析和resharper的编译时警告。
这是因为框架要求相同的两个对象必须具有相同的哈希代码。如果重写equals方法对两个对象进行特殊比较,并且该方法认为这两个对象相同,则这两个对象的哈希代码也必须相同。(字典和哈希表依赖于此原则)。
哈希代码用于基于哈希的集合,如字典、哈希表、哈希集等。此代码的目的是通过将特定对象放入特定的组(bucket)来非常快速地对其进行预排序。当需要从散列集合中检索此对象时,这种预排序非常有助于找到它,因为代码必须在一个bucket中而不是在它包含的所有对象中搜索您的对象。散列码的更好分布(更好的唯一性)更快的检索。在理想情况下,每个对象都有一个唯一的哈希代码,发现它是一个O(1)操作。在大多数情况下,它接近O(1)。
这不一定很重要;它取决于您的集合的大小和性能要求,以及您的类是否将在您可能不知道性能要求的库中使用。我经常知道我的集合大小不是很大,我的时间比通过创建一个完美的哈希代码获得的几微秒性能更有价值;因此(为了消除编译器发出的恼人警告),我只使用:
1 2 3 4
| public override int GetHashCode()
{
return base.GetHashCode();
} |
(当然,我也可以使用pragma关闭警告,但我更喜欢这种方式。)
当你处在一个你确实需要表现的位置时,当然这里其他人提到的所有问题都适用。最重要的是-否则,当从哈希集或字典中检索项时,您将得到错误的结果:哈希代码不能随对象的生命周期而变化(更准确地说,在需要哈希代码的时间内,例如当哈希代码是字典中的键时):例如,由于值是公共的,因此以下是错误的,因此可以是C在实例的生命周期内挂起到类的外部,因此不能将其用作哈希代码的基础:
1 2 3 4 5 6 7 8 9
| class A
{
public int Value;
public override int GetHashCode()
{
return Value.GetHashCode(); //WRONG! Value is not constant during the instance's life time
}
} |
另一方面,如果值不能更改,可以使用:
1 2 3 4 5 6 7 8 9
| class A
{
public readonly int Value;
public override int GetHashCode()
{
return Value.GetHashCode(); //OK Value is read-only and can't be changed during the instance's life time
}
} |
- downvoted。这是平原是错误的。即使微软MSDN。中新世/美国/图书馆/ system.object.gethashcode zwnj。&;& # 8203;价值aspx),当对象状态变更的GetHashCode是变化的方式,可能影响返回值或调用equals(),即使它在其节目GetHashCode实现例子这完全取决于人们在变化能值。
- 塞巴斯蒂安:我不同意,如果你添加对象到一个哈希码使用的收集,它是放在一个相关的哈希代码。如果你想改变你现在的哈希代码找到一个对象中收集错误的我将寻找。这是,事实上,发生在我们的代码中有个东西和那是为什么我发现它必要一点时间。
- 此外,Sebastian在gethashcode()必须更改的链接(msdn.microsoft.com/en-us/library/system.object.gethashcode.&zwnj;&8203;aspx)中看不到语句。相反,只要equals为同一个参数返回相同的值,它就不能更改:"对象的getHashCode方法必须始终返回相同的哈希代码,只要不修改确定对象equals方法返回值的对象状态。"此语句并不意味着相反,如果equals的返回值更改,则它必须更改。
- @我觉得你说的不对。msdn文章清楚地指出:"哈希代码不是一个永久值。因此:1)不要序列化哈希代码值或将其存储在数据库中。2)不要使用哈希代码作为键从键控集合中检索对象。三。。。4…",也就是说,hascode在对象的生命周期中发生了变化,您的代码需要注意这一点。如果您希望使每个对象都具有唯一的标识符,而该标识符在其生命周期中永远不会改变,那么您应该使用其他东西。
- @Joao,你把合同的客户/消费者方面与生产商/实施者混淆了。我说的是实现者的责任,他重写getHashCode()。你说的是消费者,就是使用价值的人。
- 完全误解…:)事实上,哈希代码必须在对象状态更改时更改,除非状态与对象的标识无关。此外,您不应该将可变对象用作集合中的键。为此,请使用只读对象。GetHashCode,等于…还有一些其他的方法,我现在不记得它们的名字了,它们永远不应该抛出。
据我所知,原始gethashcode()返回对象的内存地址,因此,如果要比较两个不同的对象,则必须重写它。
编辑:这是不正确的,原始的getHashCode()方法无法确保两个值相等。尽管相同的对象返回相同的哈希代码。
在我看来,下面使用反射是考虑公共属性的一个更好的选择,因为这样您就不必担心属性的添加/删除(尽管不是很常见的场景)。我发现这也表现得更好。(与使用诊断秒表的时间相比)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public int getHashCode ()
{
PropertyInfo [] theProperties = this.GetType().GetProperties();
int hash = 31;
foreach (PropertyInfo info in theProperties )
{
if (info != null)
{
var value = info .GetValue(this, null);
if(value != null)
unchecked
{
hash = 29 * hash ^ value.GetHashCode();
}
}
}
return hash ;
} |
- GetHashCode()的实现应该非常轻量级。我不确定反射是否在数千次通话中的秒表中很明显,但它肯定在数百万次通话中(想想从列表中填充字典)。