Ruby:为什么每次覆盖#eql都需要覆盖#hash?

时间:2019-03-02 17:49:11

标签: ruby

发言人在this presentation中创建了一个值类。

在实现它时,他重写了#eql?,并说在Java开发中,习惯用法是,每当重写#eql?时,都必须重写#hash

class Weight
  # ...

  def hash
    pounds.hash
  end

  def eql?(other)
    self.class == other.class &&
      self.pounds == other.pounds
  end
  alias :== eql?
end

首先,什么是#hash方法?我可以看到它返回一个整数。

> 1.hash
=> -3708808305943022538

> 2.hash
=> 1196896681607723080

> 1.hash
=> -3708808305943022538

使用撬动,我可以看到一个整数对#hash做出了响应,但是我看不到它从哪里继承该方法。未在NumericObject上定义。如果我知道此方法的作用,我可能会理解为什么需要与#eql?同时重写它。

那么,为什么#hash每次被覆盖时都需要覆盖eql?

2 个答案:

答案 0 :(得分:3)

#hash方法为接收对象返回数字hash

:symbol.hash # => 2507

Ruby哈希是哈希映射数据结构的实现,它们使用#hash返回的值来确定是否引用了相同的键。 哈希结合#eql?值和#hash方法来确定相等性。

鉴于这两种方法共同为哈希提供有关平等的信息,如果覆盖#eql?,则还需要覆盖#hash,以保持对象行为一致与其他Ruby对象结合。

如果不覆盖它,则会发生这种情况:

class Weight
  attr_accessor :pounds

  def eql?(other)
    self.class == other.class && self.pounds == other.pounds
  end

  alias :== eql?
end

w1 = Weight.new
w2 = Weight.new

w1.pounds = 10
w2.pounds = 10

w1 == w2 # => true, these two objects should now be considered equal

weights_map = Hash.new
weights_map[w1] = '10 pounds'
weights_map[w2] = '10 pounds'

weights_map # => {#<Weight:0x007f942d0462f8 @pounds=10>=>"10 pounds", #<Weight:0x007f942d03c3c0 @pounds=10>=>"10 pounds"}

如果将w1w2视为相等,则散列中应该只有一对键值对。但是,Hash类正在调用#hash,但我们并未重写。 要解决此问题并真正使w1w2相等,我们将#hash覆盖为:

class Weight
  def hash
    pounds.hash
  end
end

weights_map = Hash.new
weights_map[w1] = '10 pounds'
weights_map[w2] = '10 pounds'

weights_map # => {#<Weight:0x007f942d0462f8 @pounds=10>=>"10 pounds"}

现在哈希知道这些对象相等,因此仅存储一对键值对

答案 1 :(得分:3)

  

首先,什么是#hash方法?我可以看到它返回一个整数。

假设#hash方法返回接收者的哈希值。 (该方法的名称有点赠品)。

  

使用撬动,我可以看到一个整数对#hash做出了响应,但是我看不到它从哪里继承该方法。

[so]上有许多类型为“该方法从何而来”的问题,答案始终是相同的:知道方法从何而来的最佳方法是简单地问一下: / p>

hash_method = 1.method(:hash)
hash_method.owner #=> Kernel

因此,#hash是从Kernel继承的。但是请注意,ObjectKernel之间存在一些特殊的关系,因为在Kernel中实现的某些方法已在Object中进行了记录,反之亦然。这可能有历史原因,现在是Ruby社区中不幸的事实。

很遗憾,由于我不了解的原因,the documentation for Object#hash于2017年在commit ironically titled "Add documents"中被删除。但是,它是still available in Ruby 2.4粗体强调我的):

  

hashinteger

     

为此对象生成一个Integer哈希值。此功能必须具有a.eql?(b)暗示a.hash == b.hash 的属性。

     

Hash类将哈希值与eql?一起使用,以确定两个对象是否引用相同的哈希键。 […]

因此,正如您所看到的,#eql?#hash之间存在着深刻而重要的关系,实际上,使用#eql?和{{1}的方法的正确行为} 取决于保持这种关系的事实。

因此,我们知道该方法被称为#hash,因此很可能计算出哈希值。我们知道它与#hash一起使用,并且我们知道它特别由eql?类使用。

它到底是做什么的?好吧,我们都知道哈希函数是什么:它是一个将较大的可能无限的输入空间映射到较小的有限输出空间的函数。特别是在这种情况下,输入空间是所有Ruby对象的空间,输出空间是“快速整数”(即以前称为Hash的空间)。

我们知道哈希表是如何工作的:将值基于其键的哈希值放置在存储桶中,如果我想查找一个值,则只需要计算键的哈希值(快速),然后知道我(在恒定时间内)在哪个桶中找到值,而不是例如一个键值对数组,在这里我需要将键与数组中的每个键(线性搜索)进行比较以找到值。

但是,存在一个问题:由于散列的输出空间小于输入空间,因此存在具有相同散列值的不同对象,因此它们最终位于同一存储桶中。因此,当两个对象具有不同的哈希值时,我就知道它们是不同的事实,但是如果它们具有相同的哈希值,那么它们可能仍然是不同的,因此我需要将它们进行比较以确保相等性-这就是哈希与相等之间的关系从何而来。另外请注意,当多个键位于同一个存储桶中时,我将再次不得不将搜索键与存储桶中的每个键进行比较(线性搜索)以找到值。

由此,我们可以得出Fixnum方法的以下属性:

  • 它必须返回一个#hash
  • 不仅如此,它还必须返回一个“快速整数”(相当于旧的Integer)。
  • 对于两个相等的对象,它必须返回相同的整数。
  • 对于两个被认为不相等的对象,可能返回相同的整数。
  • 但是,这样做的可能性很小。 (否则,Fixnum可能会退化为性能大大降低的链表。)
  • 构造不相等但故意具有相同哈希值的对象也应该很困难。 (否则,攻击者可以强制 Hash退化为链接列表,作为服务质量下降攻击的一种形式。)