Menu
CoddyTech

相等性与哈希

本课属于 Coddy Ruby 学习旅程的“面向对象编程”单元。第 37 课,共 57 课。

Ruby 有几种相等性。== 表示值相等,你可以为自己的类定义它。哈希和 uniq 会改用另外两个方法:eql? 和 hash。如果只定义了 ==,相等的点仍然是不同的哈希键:

class Point
  attr_reader :x, :y

  def initialize(x, y)
    @x = x
    @y = y
  end

  def ==(other)
    other.is_a?(Point) && x == other.x && y == other.y
  end
end

a = Point.new(1, 2)
b = Point.new(1, 2)
puts a == b
puts [a, b].uniq.size
puts({ a => "first" }[b].inspect)

输出:

true
2
nil

Hash 会先比较 hash 值,也就是根据对象计算出的数字,然后用 eql? 检查候选对象。根据与 == 相同的字段定义这两个方法。alias eql? == 会复用 ==,而 [x, y].hash 会组合这些字段:

class Point
  attr_reader :x, :y

  def initialize(x, y)
    @x = x
    @y = y
  end

  def ==(other)
    other.is_a?(Point) && x == other.x && y == other.y
  end
  alias eql? ==

  def hash
    [x, y].hash
  end
end

a = Point.new(1, 2)
b = Point.new(1, 2)
puts [a, b, Point.new(3, 4)].uniq.size
puts({ a => "first" }[b])

输出:

2
first

规则是:相等的对象(eql?)必须有相同的 hash。两个不同的对象可能碰巧拥有相同的哈希值,这就是为什么仍要检查 eql?。这样,使用 Hash 计数时依据的是值,而不是对象标识:

class Word
  attr_reader :text

  def initialize(text)
    @text = text.downcase
  end

  def ==(other)
    other.is_a?(Word) && text == other.text
  end
  alias eql? ==

  def hash
    text.hash
  end
end

counts = Hash.new(0)
%w[Ruby ruby RUBY gem].each { |t| counts[Word.new(t)] += 1 }
counts.each { |word, n| puts "#{word.text}: #{n}" }

输出:

ruby: 3
gem: 1

equal?仍表示对象标识,不应被重新定义。内置值已经遵循这些规则,因此字符串和数组可以作为哈希键:

a == b          # 值相同(由你定义)
a.eql?(b)       # 用于 Hash 键和 uniq 的值相同判断(== 的别名)
a.hash          # 一个数字;相等的对象会得到相同的数字
a.equal?(b)     # 同一个对象(绝不要重新定义)
challenge icon

挑战

简单

完善 Point 类,让坐标相同的点通过 ==、eql? 和 hash 判定为相等,并可用作 Hash 的键。to_s 返回 (1, 2)。

提供的代码会按 x,y 的行读取点。它会用 uniq 输出不同点的数量,然后在 Hash 中统计每个点的访问次数,并按首次访问顺序输出为 (1, 2): 2,最后输出一个新建的 Point.new(0, 0) 是否为键。

请将代码写在 point.rb 中。main.rb 包含提供的输入输出代码,不能编辑。

亲手试试

require_relative 'point'

# 提供的输入和输出代码:保持原样。
input = []
while (line = gets)
  input << line.chomp
end
points = input.map { |line| Point.new(*line.split(",").map(&:to_i)) }
puts "distinct: #{points.uniq.size}"
visits = Hash.new(0)
points.each { |pt| visits[pt] += 1 }
visits.each { |pt, n| puts "#{pt}: #{n}" }
puts "origin visited: #{visits.key?(Point.new(0, 0))}"
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: Ruby 在线编译器