代码之家  ›  专栏  ›  技术社区  ›  user3574603

鲁比:为什么每当“eql”时都需要重写“hash”?`被覆盖?

  •  0
  • user3574603  · 技术社区  · 6 年前

    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
    

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

    > 1.hash
    => -3708808305943022538
    
    > 2.hash
    => 1196896681607723080
    
    > 1.hash
    => -3708808305943022538
    

    使用pry我可以看到一个整数响应 #散列 但是我看不出它从哪里继承了这个方法。它没有定义在 Numeric Object . 如果我知道这个方法做了什么,我可能会理解为什么它需要在 #eql? .

    那么,为什么 #散列 需要在任何时候覆盖 eql? 被覆盖?

    0 回复  |  直到 6 年前
        1
  •  4
  •   Jörg W Mittag    6 年前

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

    这个 #散列 方法应该返回接收器的哈希值。(方法的名称有点像赠品)。

    使用pry我可以看到一个整数响应 #散列 但是我看不出它从哪里继承了这个方法。

    在[so]上有几十个“这种方法从何而来”的问题,答案总是一样的:知道方法从何而来的最好方法,就是简单地问它:

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

    所以, #散列 继承自 Kernel . 不过,请注意,在 Object 内核 ,其中有些方法在 内核 记录在 对象 反之亦然。这可能有历史原因,而且现在是Ruby社区中不幸的事实。

    不幸的是,由于我不明白的原因, the documentation for Object#hash 在2017年被删除 commit ironically titled "Add documents" . 但事实上, still available in Ruby 2.4 ( 大胆的 强调我的):

    hash → integer

    生成此对象的整数哈希值。这个函数 必须拥有 a.eql?(b) 暗示 a.hash == b.hash .

    哈希值与eql一起使用? 由哈希类确定两个对象是否引用同一哈希键。[]

    所以,正如你所看到的,在 #eql? #散列 ,实际上使用 #eql? #散列 取决于 在这种关系得以维持的事实上。

    所以,我们知道这个方法叫做 #散列 因此可能会计算一个散列。我们知道它和 eql? ,我们知道它尤其被 Hash 上课。

    它到底是做什么的?好吧,我们都知道散列函数是什么:它是一个函数,将一个更大的,潜在的无限大的输入空间映射到一个更小的,有限的,输出空间。特别是,在本例中,输入空间是所有Ruby对象的空间,输出空间是“快速整数”(也就是那些过去被称为 Fixnum ).

    我们知道散列表是如何工作的:如果我想找到一个值,我只需要根据键的散列值将值放入bucket中,然后我只需要计算键的散列值(这很快),并知道我在哪个bucket中(在恒定时间内)找到值,而不是一个键值对数组,在那里我需要将键与每个键进行比较在数组(线性搜索)中查找值。

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

    由此我们可以得出 #散列 方法:

    • 它必须返回 Integer .
    • 不仅如此,它还必须返回一个“快速整数”(相当于旧的 固定值 s)。
    • 对于被视为相等的两个对象,它必须返回相同的整数。
    • 可以 对于被认为不相等的两个对象,返回相同的整数。
    • 然而 ,它只能以低概率这样做。(否则 搞砸 可能退化为具有高度降级性能的链接列表。)
    • 也应该是 坚硬的 构造不相等但故意具有相同哈希值的对象(否则,攻击者可以 搞砸 以降级服务攻击的形式降级为链接列表。)
        2
  •  3
  •   Fito von Zastrow    6 年前

    这个 #hash 方法返回数值 hash 接收对象的值:

    :symbol.hash # => 2507
    

    Ruby散列是散列映射数据结构的实现,它们使用 #散列 以确定是否正在引用同一密钥。 散列利用 #eql? 方法与 #散列 用于确定相等性的值。

    假设这两个方法协同工作以提供散列的相等信息,如果 #eql? ,您还需要覆盖 #散列 保持你的目标的行为 一致的 以及其他的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"}
    

    如果 w1 w2 如果被认为是相等的,则哈希中应该只有一个键值对。但是,Hash类正在调用 #散列 我们没有推翻。 为了解决这个问题 宽1 第二周 等于,我们重写 #散列 致:

    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"}
    

    现在hash知道这些对象是相等的,因此只存储一个键值对