LoginSignup
2

More than 5 years have passed since last update.

Rubyのクラスで`==`を独自定義するとArray#uniqが期待通りに動かない

Last updated at Posted at 2017-04-13

クラスを定義したとき、作成したインスタンス同士が等しいかどうかを判定するために==を定義することは多いかと思いますが、==のみを定義してもArray#uniq等の一部比較が意図したとおりに行われません。

失敗するバージョン

クラス定義

このクラスではunique_idが等しいかどうかを比較して同一性を決定するようにしています。

class Hoge
  attr_accessor :unique_id

  def initialize(uid)
    @unique_id = uid
  end

  def ==(other)
    @unique_id == other.unique_id
  end

  # 出力結果をわかりやすくするためにオーバーライド
  def to_s
    @unique_id
  end
end

通常比較(成功する)

特に特別なことはありませんね。==を定義したことにより、hoge1とhoge4は同じものとみなされるようになりました。

hoge1 = Hoge.new('aaa')
hoge2 = Hoge.new('bbb')
hoge3 = Hoge.new('ccc')
hoge4 = Hoge.new('aaa')

hoge1 == hoge2
=> false

hoge1 == hoge3
=> false

hoge1 == hoge4
=> true

uniq/uniq!(失敗する)

hoge1hoge4は同じunique_idなので、uniqしたら重複排除されるともーじゃん?

hoge1 = Hoge.new('aaa')
hoge2 = Hoge.new('bbb')
hoge3 = Hoge.new('ccc')
hoge4 = Hoge.new('aaa')

array = [hoge1, hoge2, hoge3, hoge4]

array.uniq!
puts array # 期待する結果はhoge1, hoge2, hoge3の3つ
------------------------------------------
# 出力: なんで4つあるんや!
# aaa
# bbb
# ccc
# aaa
------------------------------------------

されねーんです。

成功するバージョン

クラス定義

結論から言うと、==の他にeql?hashメソッドもオーバーライドしないといけません。

hashメソッドについては@unique_idを元にハッシュ値を生成することで、同じ@unique_idなら必ず同じハッシュ値が生成されるようになります。
eql?メソッドについてはメソッドだけ用意して、中身は単にオーバーライドした==のエイリアスとして動作するようにします。

class Hoge
  attr_accessor :unique_id

  def initialize(uid)
    @unique_id = uid
  end

  def hash
    @unique_id.hash
  end

  def eql?(other)
    self == other
  end

  def ==(other)
    @unique_id == other.unique_id
  end

  def to_s
    @unique_id
  end
end

追記@scivolaさん、@jnchitoさん コメントありがとうございます。こういうことですね!

(uniqはインスタンスのハッシュ値が等しいかどうかでオブジェクトが等しいかどうかを判定しているのかな?でもそれなら==hashだけオーバーライドすればいいと思うんだけど、eql?を定義しないとやっぱりうまくいかない。この辺詳しい人いたら教えて下さい。)

  • uniqの判定にはeql?が使用される
    公式リファレンスマニュアル Array#uniq

    要素の重複判定は、Object#eql? により行われます。

  • eql?をオーバーライドするときは、hashをセットでオーバーライドする必要がある
    公式リファレンスマニュアル Object#eql?

    このメソッドを再定義した時には Object#hash メソッ ドも再定義しなければなりません。

uniq/uniq!(成功する)

hoge1hoge4は同じunique_idなので、uniqしたら重複排除されるともーじゃん?

hoge1 = Hoge.new('aaa')
hoge2 = Hoge.new('bbb')
hoge3 = Hoge.new('ccc')
hoge4 = Hoge.new('aaa')

array = [hoge1, hoge2, hoge3, hoge4]

array.uniq!
puts array # 期待する結果はhoge1, hoge2, hoge3の3つ
------------------------------------------
# 出力:
# aaa
# bbb
# ccc
------------------------------------------

されるやん!!!いけるやん!!!

というわけで、uniq/uniq!でハマった話でした。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2