Ruby

【Ruby】浅いコピーと深いコピー

おはようございます。
今回は浅いコピーと深いコピーについて知らず、少し調べるとコピーでも様々あり浅いコピーや深いコピーについて知っておかないとバグを生みそうだなと思ったのでまとめていきたい思います。

= による複製

= を使った複製では両者は名前が違うだけの同じオブジェクトを参照していることになります。

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

ABOUT ME
sakai
東京在住の30歳。元々は車部品メーカーで働いていてましたが、プログラミングに興味を持ちスクールに通ってエンジニアになりました。 そこからベンチャー → メガベンチャー → 個人事業主になりました。 最近は生成 AI 関連の業務を中心にやっています。 ヒカルチャンネル(Youtube)とワンピースが大好きです!