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?
を考慮する必要がある