この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。
Rubyでオブジェクトを複製する際、知らないとハマってしまいそうなポイントをメモ。
その1:=で代入されるのはオブジェクトの参照
=で代入後、破壊的メソッドを使う場合は注意が必要です。
list1 = ['a', 'b', 'c']``
list2 = list1
list1[0] = 'aa'
list1
=> ["aa", "b", "c"]
list2
=> ["aa", "b", "c"]
list1の変更がlist2にも反映されてしまいました。
初学時に混乱しがちなパターンですが、
list2 = list1
は、list2にlist1の参照を代入する、の意なので、
どちらも同じオブジェクトを参照することになります。
object_idを確認すると、同じオブジェクトを参照していることが分かります。
list1.object_id
=> 70343473882920
list2.object_id
=> 70343473882920
破壊的にオブジェクトの内容が変更された場合、
そのオブジェクト見ている他の変数にも変更内容が反映されます。
その2:dup、cloneは浅いコピー
これを回避するためにはdup、cloneを使います。
が、いくつか注意点があります。
まずはdup、cloneとも別のオブジェクトとして複製されることを確認。
list3 = [{key:'xx'},{key:'yy'},{key:'zz'}]
list4 = list3.dup
list5 = list3.clone
list3.object_id
=> 70343473688700
list4.object_id
=> 70343473670940
list5.object_id
=> 70343473655820
参照しているオブジェクトが違うので、
list3の変更はlist4, list5に影響しません。
list3.reverse!
=> [{:key=>"zz"}, {:key=>"yy"}, {:key=>"xx"}]
list4
=> [{:key=>"xx"}, {:key=>"yy"}, {:key=>"zz"}]
list5
=> [{:key=>"xx"}, {:key=>"yy"}, {:key=>"zz"}]
しかしこうするとどうでしょう。
list3[0][:key]='ZZ'
list3
=> [{:key=>"ZZ"}, {:key=>"yy"}, {:key=>"xx"}]
list4
=> [{:key=>"xx"}, {:key=>"yy"}, {:key=>"ZZ"}]
list5
=> [{:key=>"xx"}, {:key=>"yy"}, {:key=>"ZZ"}]
それぞれ別オブジェクトなのにlist3の変更がlist4, list5にも反映されてしまいました。
dup, cloneは、複製するオブジェクト自体は別物になりますが、
それに含まれる参照先はそのままコピーされるため、
その1と同じ事象が発生してしまいます。(参照先のobject_idが同じ)
list3[0].object_id
=> 70343473688720
list4[2].object_id
=> 70343473688720
list5[2].object_id
=> 70343473688720
これは「浅いコピー」といいます。
dupとcloneはほぼ同じですが、以下の点が異なります。
複製するもの
dup
:汚染状態、信頼状態
clone
:汚染状態、信頼状態、凍結状態、特異メソッド
複製できないオブジェクト
true、false、nil、シンボル、数値
■リファレンス
https://docs.ruby-lang.org/ja/latest/class/Object.html#I_CLONE
その3:Marshalで深いコピー
Marshalを使うとこの問題が解決します。
Marshalはオブジェクトをシリアライズ/デシリアライズするモジュールで、
いったん文字列(ファイル)として保存したものを読み戻すため、
まったく別のオブジェクトとして複製することができます。
Marshal.load(Marshal.dump(obj))
list6 = Marshal.load(Marshal.dump(list3))
list3[1][:key]='YY'
list3
=> [{:key=>"ZZ"}, {:key=>"YY"}, {:key=>"xx"}]
list6
=> [{:key=>"ZZ"}, {:key=>"yy"}, {:key=>"xx"}]
■リファレンス
https://docs.ruby-lang.org/ja/latest/class/Marshal.html
破壊的メソッドを使う際には、
うっかり複製元/先に変更が影響してしまわないよう
注意が必要です。