【一人 bugs.ruby Advent Calendar 2020】[Bug #17058] Array#delete_if doesn't change array instantly【8日目】

一人 bugs.ruby Advent Calendar 2020 8日目の記事になります。

[Bug #17058] Array#delete_if doesn't change array instantly

さて Array#delete_if で条件にあった要素を削除する事ができます。

a = [1, 2, 3, 4, 5, 6]
# 偶数を削除する
a.delete_if { |it|
  it.even?
}
p a
# => [1, 3, 5]

このメソッドのドキュメントでは『ブロックが呼ばれるたびにレシーバに即座に反映される』と書かれています。
しかし、実際の挙動は #delete_if の処理が終わったあとに反映されるような挙動になっています。

a = [1, 2, 3, 4, 5, 6]
# 偶数を削除する
a.delete_if { |it|
  puts "During iteration: #{a}"
  it.even?
}
p a
# 期待する挙動:
# During iteration: [1, 2, 3, 4, 5, 6]
# During iteration: [1, 2, 3, 4, 5, 6]
# During iteration: [1, 3, 4, 5, 6]
# During iteration: [1, 3, 4, 5, 6]
# During iteration: [1, 3, 5, 6]
# During iteration: [1, 3, 5, 6]
# [1, 3, 5]

# 実際の動作:
# During iteration: [1, 2, 3, 4, 5, 6]
# During iteration: [1, 2, 3, 4, 5, 6]
# During iteration: [1, 2, 3, 4, 5, 6]
# During iteration: [1, 3, 3, 4, 5, 6]
# During iteration: [1, 3, 3, 4, 5, 6]
# During iteration: [1, 3, 5, 4, 5, 6]
# [1, 3, 5]

ちょっとわかりづらいんですが、上の例の実際の動作では『即座に要素を削除していく』のではなくて『配列の先頭から結果をどんどん書き換えていく』というような動作になっています。
この挙動は Ruby 2.2 -> 2.3 で変更されたようです。
このチケットでは『挙動を直す』のではなくて『ドキュメントを修正する』という対応がなされました。
『レシーバを変更中にレシーバを参照するのはよくない』っていうやつですねー。
普段こんなコードを書かないんですが、実際にレシーバの中身を見てみると Ruby がどんな処理を行っているのか垣間見る事ができて面白いですね。