Menu
Coddy logo textTech

Равенство и хеширование

Часть раздела Объектно-ориентированное программирование путешествия по Ruby на Coddy. Урок 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 — числа, вычисленные на основе объекта, — а затем проверяет кандидатов с помощью 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