(これに2時間気づかず時間を浪費した腹いせに、記事を書いています。)
配列内のオブジェクトを1つだけ変更したつもりが全部変わっている罠
nested_array = Array.new(2, [])
pp nested_array
# [[], []]
なんてことはない、二重配列ですね。この1つ目の子ども配列に適当な値をpushしてみます。
nested_array = Array.new(2, [])
pp nested_array
nested_array[0].push('テキスト')
pp nested_array
# [[], []]
# [["テキスト"], ["テキスト"]]
ちゃんと1つ目の子ども配列を指定したにも関わらず、両方の子ども配列に要素が追加されてしまいました。
Array.newで複数作るオブジェクトは同じオブジェクトとして扱われる
nested_array = Array.new(2, [])
pp nested_array[0].object_id
pp nested_array[1].object_id
# 60
# 60
text_array = Array.new(2, 'テキスト')
pp text_array[0].object_id
pp text_array[1].object_id
# 70
# 70
こんなふうに、Array.newで作られるオブジェクトは同じオブジェクトIDを持ちます。したがって、破壊的変更をどちらかで行うと、見かけ上すべてのオブジェクトが変更されるというカラクリでした。
公式ドキュメントより
new(size = 0, val = nil) -> Array
長さ size の配列を生成し、各要素を val で初期化して返します。
要素毎に val が複製されるわけではないことに注意してください。全要素が同じオブジェクト val を参照します。後述の例では、配列の各要素は全て同一の文字列を指します。
text_array = Array.new(2, 'テキスト')
text_array[0].concat('を破壊的に変更')
pp text_array
# ["テキストを破壊的に変更", "テキストを破壊的に変更"]
初期値が必要で、かつ配列内のオブジェクトは別々のものとして扱いたい場合、おとなしく順番に詰め込んでいきましょう。
→こういった場合のセオリーをコメントにて教えていただきました。
こういうケースではArray.newにブロックを渡すのが定石です。
nested_array = Array.new(2) { [] }
pp nested_array
#=> [[], []]
nested_array[0].push('値')
pp nested_array
#=> [["値"], []]
公式ドキュメントでも以下のように記載があります。
new(size) {|index| ... } -> Array
長さ size の配列を生成し、各要素のインデックスを引数としてブロックを実行し、各要素の値をブロックの評価結果に設定します。
ブロックは要素毎に実行されるので、全要素をあるオブジェクトの複製にすることができます。