Ruby で任意の異なるクラスのオブジェクトを #uniq するときの注意
元ネタ
Rubyむずい…うむむむむむむむうむ
— 異常者 (@h1manoa) 2017年6月8日
[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ https://t.co/buooxLKDHW
と、いうことでちょっとやってみたら見事にハマったので覚書
元のコード
class Hoge def initialize(num) @num = num end def ==(other) if @num == other true else false end end end p [Hoge.new(1), Hoge.new(2), 1, 2, 5 ,6].uniq # => [#<Hoge:0x000000024871f8 @num=1>, #<Hoge:0x000000024871d0 @num=2>, 1, 2, 5, 6]
元のコードはユーザ定義クラスと Integer が入り混じった配列をいい感じに #uniq したいという内容でした。
#uniq の条件
#uniq は要素の Object#eql? を使用して重複を判定します。
また、 #eql? を定義した場合は Object#hash も再定義する必要があります。
と、いうことでこれに沿って修正してみました。
class Hoge def initialize(num) @num = num end def hash @num.hash end def eql?(other) if @num == other true else false end end end p [Hoge.new(1), Hoge.new(2), 1, 2, 5 ,6].uniq # => [#<Hoge:0x000000024871f8 @num=1>, #<Hoge:0x000000024871d0 @num=2>, 1, 2, 5, 6]
しかし、これでもまだ意図した動作になりません。
#hash と #eql? は双方向で true になる必要がある
調べていたらわかったんですが
Hoge.new(1).eql? 1 # => true
だけではなくて
1.eql? Hoge.new(1).eql? # => true
のように双方向で true になる必要があるみたいです(当然 #hash に関しても同様
完成
と、言うことで Integer#eql? を拡張することで無事に意図する動作になりました。
class Hoge def initialize(num) @num = num end def hash @num.hash end def eql?(other) if @num == other true else false end end end class Integer def eql?(other) return super(other) unless Hoge === other other.eql? self end end _1 = Hoge.new(1) # この条件をすべて満たす必要がある p _1.hash == 1.hash p _1.eql? 1 p 1.hash == _1.hash p 1.eql? _1 p [Hoge.new(1), Hoge.new(2), 1, 2, 5 ,6].uniq # => [#<Hoge:0x000000024871f8 @num=1>, #<Hoge:0x000000024871d0 @num=2>, 5, 6]
流石に #uniq のためだけに Integer をクラス拡張するのはツラいですね…。
まとめ
#uniqは#eql?を参照して重複のチェックを行う#eql?を書き換えた場合は#hashも書き換える必要がある#uniqは双方向でチェックするので要素すべてのオブジェクトに対して#eql?を考慮する必要がある