おはようございます。
今回は浅いコピーと深いコピーについて知らず、少し調べるとコピーでも様々あり浅いコピーや深いコピーについて知っておかないとバグを生みそうだなと思ったのでまとめていきたい思います。
= による複製
= を使った複製では両者は名前が違うだけの同じオブジェクトを参照していることになります。
sample = "hoge"
=> "hoge"
copy_sample = sample
=> "hoge"
sample.upcase!
=> "HOGE"
copy_sample
=> "HOGE"
sample.object_id
=> 69980
copy_sample.object_id
=> 69980
浅いコピー(シャローコピー)
Rubyにはオブジェクトを複製するメソッドとしてdup(clone)
メソッドが用意されています。
sample = "hoge"
copy = sample.dup
sample = "fuga"
sample
=> "fuga"
copy
=> "hoge"
sample.object_id
=> 180
copy.object_id
=> 200
しかし、このコピーは浅いコピーであることに注意する必要があります。浅いコピーで挙動に影響が出るのはコピー元が階層構造を持っている場合です。
sample_arr = ["hoge", "fuga"]
copy_arr = sample_arr.dup
sample_arr.object_id
=> 220
copy_arr.object_id
=> 240
sample_arr.first.gsub!("hoge", "piyo")
sample_arr
=> ["piyo", "fuga"]
# この場合、copy元は["hoge", "fuga"]であることが期待される
copy_arr
=> ["piyo", "fuga"]
このように浅いコピーの場合、オブジェクト自体はコピーされますが、その中身は元のオブジェクトと同じ参照をしていることが分かります。
深いコピー
深いコピーを行うのに一番楽な方法はactive_supportのメソッドを使うことです。railsコンソールを立ち上げて確認します。
copy_arr = sample_arr.deep_dup
=> ["hoge", "fuga"]
sample_arr.first.gsub!("hoge", "piyo")
=> "piyo"
sample_arr
=> ["piyo", "fuga"]
copy_arr
=> ["hoge", "fuga"]
コピー元の参照先もコピーされていることが分かります。階層を深くした場合はどうでしょうか。
sample_arr = [["りんご", "メロン"], "味噌汁", "ご飯", "野菜"]
=> [["りんご", "メロン"], "味噌汁", "ご飯", "野菜"]
copy_arr = sample_arr.deep_dup
=> [["りんご", "メロン"], "味噌汁", "ご飯", "野菜"]
sample_arr.first.first.gsub!("りんご", "いちご")
sample_arr
=> [["いちご", "メロン"], "味噌汁", "ご飯", "野菜"]
copy_arr
=> [["りんご", "メロン"], "味噌汁", "ご飯", "野菜"]
階層が深くなってもその参照先もコピーできていることが分かります。どれだけ深くなってもコピーできそうです。
また、Marshalモジュールを使っても深いコピーを行うことができます。MarshalモジュールとはRubyオブジェクトを文字列化することができるものです。ファイルの書き出しや読み出しによく使われるそうです。
sample_arr = ["hoge", "fuga"]
=> ["hoge", "fuga"]
tmp = Marshal.dump(sample_arr)
=> "\x04\b[\aI\"\thoge\x06:\x06ETI\"\tfuga\x06;\x00T"
copy = Marshal.load(tmp)
=> ["hoge", "fuga"]
sample_arr.first.gsub!("hoge", "piyo")
sample_arr
=> ["piyo", "fuga"]
copy
=> ["hoge", "fuga"]
一度、オブジェクトを文字列化してから、再度オブジェクトに戻すことで元のオブジェクトを完全にコピーすることができます。正規の方法ではないような気もしますが、公式でも紹介されているので問題なさそうです。
インスタンスをコピーする場合
インスタンスの浅いコピー(シャローコピー)をする場合、その属性のオブジェクトはきちんとコピーがされ、別で参照先が作られます。
show-medels
Publisher
id: integer
name: string
created_at: datetime
updated_at: datetime
publisher = Publisher.create!(name: "太宰治")
=> #<Publisher:0x00007fb1dc2b6b10 id: 2, name: "太宰治", created_at: Sat, 24 Apr 2021 23:23:54.213673000 UTC +00:00, updated_at: Sat, 24 Apr 2021 23:23:54.213673000 UTC +00:00>
copied_publisher = publisher.dup
=> #<Publisher:0x00007fb1db4d31d8 id: nil, name: "太宰治", created_at: nil, updated_at: nil>
publisher.name = "芥川龍之介"
=> "芥川龍之介"
copied_publisher.name
=> "太宰治"
publisher.name.object_id
=> 70200933169540
copied_publisher.name.object_id
=> 70200935904020
インスタンスの属性値にオブジェクトや配列が保存されていることはまずないので、(あったら正規化すべき)浅いコピーや深いコピーについてそれほど深く考える必要はなさそうです。
危険なのはインスタンスではなく、配列やオブジェクトをコピーするケースになりそうです。
さいごに
浅いコピーや深いコピーについて知っておかないと、思わぬバグにつながるケースもあると思うので気をつけたいと思いました。
ここまで読んでいただきありがとうございました。
参考
https://qiita.com/ricoirico/items/5cfcac1b8e67184641f1