シリーズ2の1日目です!卒業生とメンターさんがコラボして書いています📝
まずは、これを見てください。
numbers = [1,2,3]
numbers += [4]
p numbers # [1, 2, 3, 4]
numbers = [1,2,3]
numbers << [4]
p numbers # [1, 2, 3, 4]
一見、配列に要素を追加しただけに見えます。
しかし、Object#freezeを使ってみると以下のような違いが出てきます。
numbers = [1,2,3]
numbers.freeze
# numbers << [4] # can't modify frozen Array: [1, 2, 3] (FrozenError)
# numbers += [4] # 実行してもエラーにならない
初めてこの違いに気づいた時、調査をしてみました🔍
間違い・不足している部分などがあればご指摘いただけますと幸いです。
+ の行っていること
Arrayには+メソッドがあります。
自身と other の内容を繋げた配列を生成して返します。
[PARAM] other:
自身と繋げたい配列を指定します。配列以外のオブジェクトを指定した場合は to_ary メソッドによる暗黙の型変換を試みます。
Array#+ (Ruby 3.2 リファレンスマニュアル) から引用
説明を読んでみると、自身とotherの繋げた配列を生成して返すとのことです。
サンプルコードも載っていました。
a = [1, 2]
b = [8, 9]
p a + b #=> [1, 2, 8, 9]
p a #=> [1, 2] (変化なし)
p b #=> [8, 9] (こちらも変化なし)
元の配列に変化はなく、新たに配列ができているということですね!
一応、object_idもチェックしてみましょう。
numbers = [1,2,3]
p numbers.object_id # 260
numbers += [4]
p numbers.object_id # 280
IDが違いますね。ということは元のオブジェクトには影響を与えず、新しいオブジェクトになっていることがわかりました。
<< の行っていること
では次にArray#<<を見てみましょう。
ドキュメントの説明を読んでみると、下記のように書いてあります。
指定された obj を自身の末尾に破壊的に追加します。
「破壊的に」とは、class String (Ruby 3.2 リファレンスマニュアル)で説明がありました。
--- ここから引用 ---
「破壊的な変更」とは、あるオブジェクトの内容自体を変化させることです。例えば文字列のすべての文字を破壊的に大文字へ変更する String#upcase! メソッドの使用例を以下に示します。
例:String#upcase!
a = "string"
b = a
a.upcase!
p a # => "STRING"
p b # => "STRING"
--- ここまで引用 ---
つまり、<<
で要素を追加しようとすると、レシーバのオブジェクトの末尾に、オブジェクトの内容を変更する形で追加されるのですね。
実際にやってみます。
numbers = [1,2,3]
p numbers.object_id # 260
numbers << 4
p numbers # [1, 2, 3, 4]
p numbers.object_id # 260
「4」の要素追加前と後でobject_id
が変わっていませんね。
同じオブジェクトの値を変更したことがわかりました。
まとめ
一見、どちらのメソッドも結果の出力は同じですが内部で行われていることが違うことがわかりました。
特に、<<
の方は破壊的な変更を加えているので、freezeを使用した後にこのメソッドを使うとエラーになったんですね。
逆に、+
だと別のオブジェクトが生成されるから大丈夫だったということがわかりました。
このようなメソッドの挙動の違いに注意して、使用する際はなぜそのメソッドを選択したか説明できるようにしていきましょう!
追記:速度を測定してみた
Xでアドバイスをいただいて興味が湧いたので、実行速度を計算してみました!
require 'benchmark'
p RUBY_VERSION
numbers1 = [1, 2, 3]
numbers2 = [1, 2, 3]
n = 10000
Benchmark.bm do |x|
x.report("+=: ") { n.times { numbers1 += [4] } }
x.report("<<: ") { n.times { numbers2 << 4 } }
end
"3.1.2"
user system total real
+=: 0.082590 0.031661 0.114251 ( 0.114364)
<<: 0.000419 0.000222 0.000641 ( 0.000640)
<<
の方が+=
より高速であることがわかりました!🎉
+=
の方は上述したように、新しい配列を作成し元の配列に追加するため、より多くのメモリや処理の時間が必要だからではないかと考えました。何をしたいかにもよりますが、これら観点を持って判断したいと思います。