Array#uniq と Object#eql? と Object#hash
常識なのかもしれないけど、ちょっとだけはまったので、メモ。
Array#uniq は配列から重複した要素を取り除いた新しい配列を返すメソッド。
["hoge", "fuga", "piyo", "piyo", "hoge", "fuge"].uniq #=> ["hoge", "fuga", "piyo", "fuge"]
ただし、自前で定義したクラスのオブジェクトについては、そのままじゃうまく動いてくれない。
class Vector2D attr_reader :x, :y def initialize(x, y) @x, @y = x, y end def inspect "#<#{@x}, #{@y}>" end end
ary = [[3, 4], [4, 3], [1, 2], [3, 4], [1, 2]].map{|x, y| Vector2D.new(x, y)} #=> [#<3, 4>, #<4, 3>, #<1, 2>, #<3, 4>, #<1, 2>] ary.uniq #=> [#<3, 4>, #<4, 3>, #<1, 2>, #<3, 4>, #<1, 2>]
Array#uniq の
要素の重複判定は、Object#eql? により行われます。
また、Object#eql? は
各クラスの性質に合わせて再定義すべきです。多くの場合、 == と同様に同値性の判定をするように再定義されています
ということなので、この場合、Vector2D#eql? を定義してやればおっけー?
class Vector2D def eql?(other) @x.eql?(other.x) && @y.eql?(other.y) end end
ちゃんと同値判定ができるようになりました。
ary[0].eql?(ary[1]) #=> false ary[0].eql?(ary[3]) #=> true
が、
ary.uniq
#=> [#<3, 4>, #<4, 3>, #<1, 2>, #<3, 4>, #<1, 2>]
かわらんなぁ…。
実は Object#eql? には、こんな注意書きもあったのですが、まぁ、今回は関係ないよね、とか思ってたのでした。
Hash で二つのキーが等しいかどうかを判定するのに使われます。
(中略)
このメソッドを再定義した時には Object#hash メソッドも再定義しなければなりません。
Vector2D#hash を定義してやれば期待通りに動きます。Array#uniq の実装で Hash を使ってるのかな?
なお、Object#hash は
A.eql?(B) ならば A.hash == B.hashの関係を必ず満たしていなければいけません。
ということですが、この場合は
class Vector2D def hash [@x, @y].hash end end
これでおっけー。
ary.uniq
#=> [#<3, 4>, #<4, 3>, #<1, 2>]
できました。