Menu
CoddyTech

等価性とハッシュ

CoddyのRubyジャーニー「オブジェクト指向プログラミング」セクションの一部。レッスン 37/57。

Rubyにはいくつかの種類の等価性があります。==は値の等価性で、クラスで定義します。ハッシュとuniqは、代わりに別の2つのメソッド、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を持たなければなりません。異なる2つのオブジェクトが偶然同じハッシュ値を持つこともあるため、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

チャレンジ

簡単

座標が同じ点は==、eql?、hashで等しいと判定され、Hashのキーとして機能するように、クラスPointを完成させてください。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オンラインコンパイラ