その他
    ホーム 技術発信 DoRuby Rubyのオブジェクトコピーで気をつけること
    Rubyのオブジェクトコピーで気をつけること
     

    Rubyのオブジェクトコピーで気をつけること

    この記事はアピリッツの技術ブログ「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

    破壊的メソッドを使う際には、
    うっかり複製元/先に変更が影響してしまわないよう
    注意が必要です。