c#中equal方法与gethashcode方法之间有何关联?
文章目录
- 前言
- 一、对hash运算的深入思考
- 二、equal与gethashcode的关联
- 三、 equal与gethashcode不同步的后果
- 四、 规范的重写gethashcode
前言
大家有没有遇到过,当你重写了c#对象的equal方法之后,编译器会提示你对相应的gethashcode进行重写,你是否感到疑惑?这两者之间有什么关联?为什么要有这样的规范?不重写又会发生什么问题?
一、对hash运算的深入思考
我们都知道hash运算是将一个给定大小的数据经过特殊的位运算输出为一个固定位数大小的数据,hash运算的输入可以是任意的,这意味着其本质上是用有限位数的空间来表示无限范围的数据,可将其理解为数据压缩(摘要)。这决定了hash运算产生冲突是其自身的必然特性。
对于字典,哈希集这一类与hashcode相关的数据结构中,我们通常所说的hash冲突实际分为两种情况,一种情况是真冲突,即hash函数的冲突;还有一种情况是hash槽位的冲突。第二种情况是如何产生的?
我们知道c#种一个对象的hashcode是32位,即hashcode的取值从0-2^32,这是一个非常庞大的取值范围,如果我们直接以对象的hashcode作为槽位索引,那所需的内存开销无疑是巨大的,所以,通常情况下我们会对hash之后的结果再进行一次运算,来映射到槽位,一般是对其进行取余运算。这就导致会有不同的hash值落在同一个槽位中。通过这种方式,牺牲hash的精度换取存储空间。
落在同一槽位中的hash会以链表的形式进行组织,并且每个节点中会存储原始hash,便于后续精确比较。
字典中查找一个key的过程大致如下:获取key的hash–>对hash取余获取槽位索引–>链表中依次比较hash值进行粗略查找–>若hash相同进行精确的地址比较(equal方法的默认实现)
上述我将hash函数与取余分为两个过程,但实际上也可将其理解为,取余运算是hash函数的一部分,及整个计算的结果hash值就是key的槽位索引,即hash的精度进一步被弱化了,所以碰撞更多了。并且因为hash计算引入了余数这个随机因子,这会给数据结构的动态扩容产生很多问题,有一些解决方案如采用一致性hash,这里不再展开。
二、equal与gethashcode的关联
为什么c#要规定equal与gethashcode要保持同步重写呢?
我们知道c#默认这两个方法的实现,equal是对对象地址进行比较,gethashcode也是基于地址计算,也就是说它们都依赖于地址进行计算,从这里也可以看出这两个方法其实是互相绑定的,可以说不考虑冲突的情况下,hashcode与对象地址是一一对应的,hashcode就近似好比一个对象的唯一id。
c#约定相同的对象,计算的hash值必须也是一样的,反过来则不一定成立
如上所述,equal用于对两个对象进行精确比较,而在这之前hashcode用于对对象粗略筛选,这便是它们产生关联的关键所在,试想如果没有这样的约定,会产生什么影响?
三、 equal与gethashcode不同步的后果
举个例子,假如你重写了一个对象的equal方法让两个位于不同地址的对象逻辑相等,却没有重写gethashcode,假如你已经将其中一个对象存入字典当中,考虑在一个字典中的查找操作,使用另外一个逻辑相等的对象为key进行查找,逻辑上字典中应该是包含的,但是因为没有重写gethashcode,依然采用默认的基于地址计算,这就导致字典在第一次计算hash槽位时便计算错误,若槽位中有元素还会进行很多无意义的比较。
当然实际的后果还不止于此,很多依赖对象hashcode的底层结构都会受到影响而造成隐患,如hashset可能允许相同的元素存入,groupby操作将逻辑相同的元素分到了不同组等等。
四、 规范的重写gethashcode
- 进行hash计算涉及的字段应与equal方法涉及的字段保持一致,因为是这些字段作为对象的唯一标识(主键)。
- 涉及的字段尽量是不可变的对象的固有属性,hash一旦生在对象的生存周期内不应再变化,不然会影响字典操作,java中对象hash生成会直接存入对象头
3.保证hash质量,hash函数设计尽可能避免冲突产生