💡
[Bug #22013] Array#| で要素の合計が17以上の場合に #eql? で比較されないバグ報告
[Bug #22013] Array#| deduplication via eql? breaks when total element count exceeds ~16
-
Array#|はレシーバと引数の配列の要素をすべて含む配列を返すメソッドになる - ただし、『重複している要素』は取り除く
# 配列の要素をまとめつつ、同じ要素は重複しないようにする
p [1, 3, 5, 7] | [1, 2, 3, 4, 5]
# => [1, 3, 5, 7, 2, 4]
- また、各配列にすでに重複している要素もすべて取り除かれる
p [1, 1, 2, 2, 3, 3, 3] | [4, 4]
# => [1, 2, 3, 4]
- この
Array#|なのですが次のように#eql?メソッドを定義しているオブジェクトに対して『要素の合計が17以上の場合に正しく動作しない』というバグ報告になる
class Item
attr_reader :id, :pos
def initialize(id, pos:)
@id = id
@pos = pos
end
def inspect
"item-#{id}-#{pos}"
end
# id でのみ要素を比較する
def eql?(other)
id == other.id
end
end
# pos は違うが id は同じオブジェクトを用意する
items1 = [Item.new(1, pos: 1)]
items2 = [Item.new(1, pos: 2)]
pp items1.eql? items2 # => true
# 要素の合計数が 16以下の場合は意図する挙動になる
pp (items1 * 8) | (items2 * 8)
# => [item-1-1]
# 要素の合計数が 17以上の場合は重複する要素が取り除かれない
pp (items1 * 8) | (items2 * 9)
# 期待する値 => [item-1-1]
# 実際の値 => [item-1-1, item-1-2]
- これなんですが、これだけみるとバグっぽいように見えるんですが実際には
Array#|では#eql?だけではなくて#hashメソッドも適切に定義する必要があるようですね - なので次のように
Item#hashメソッドを定義すると期待する挙動になる
class Item
attr_reader :id, :pos
def initialize(id, pos:)
@id = id
@pos = pos
end
def inspect
"item-#{id}-#{pos}"
end
def eql?(other)
id == other.id
end
# #hash メソッドも定義する
def hash
[self.class, id].hash
end
end
items1 = [Item.new(1, pos: 1)]
items2 = [Item.new(1, pos: 2)]
# 要素の合計が17以上でも期待する結果になる
pp (items1 * 8) | (items2 * 9) # => [item-1-1]
pp (items1 * 17) | (items2 * 17) # => [item-1-1]
- 要素の数によって比較するメソッドを切り替えるようになっているんですね、へえ
- この比較メソッドは
#|だけではなくて#unionや#uniqでも同じような形になっているみたい - なので、バグではなかったんですがわかりづらいということで以下のようにドキュメントに追記することで対応されました
- ちなみにるりまだとちゃんと
要素の重複判定は、Object#eql? と Object#hash により行われます。と記述されていたりする
Discussion