Menu
Coddy logo textTech

等価性とハッシュ

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 を持たなければなりません。異なるオブジェクトが偶然同じハッシュ値を共有することはあります。そのため、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オンラインコンパイラ