86
Help us understand the problem. What are the problem?

posted at

updated at

[ruby]浅いコピーと深いコピー

about

shallow copy(浅いコピー)だdeep copy(深いコピー)だとよくわからないままrubyを使ってたらまんまと引っかかったので備忘録として書いてみる。間違っていたらご指摘ください:bow:

= による複製

浅い・深いの関係ないところとして、=によってオブジェクトを複製すると両者は名前が違うだけの、同じ中身を参照している関係になります。
なので、破壊的変更を加えると複製元も先も変わります。


origin = "hoge"
=> "hoge"

copy = origin
=> "hoge"

origin.object_id
=> 70347211263140

copy.object_id
=> 70347211263140

orig.equal? copy
=> true

copy.gsub!("hoge", "fuga")
=> "fuga"

origin
=> "fuga"

dupclone

rubyにはオブジェクトの複製メソッドとして、dupcloneというメソッドが用意されています。

origin = "hoge"
=> "hoge"

copy = origin.dup
=> "hoge"

origin.object_id
=> 70118270776180

copy.object_id
=> 70118270801780

上記のように、object_idが異なるので、別物として作られていることがわかります。
そのため、この変数に破壊的メソッドを実行してもお互いに影響を及ぼすことはありません。

しかし、一つ気をつけなければならないのは、この2つのメソッドが生み出すコピーが「浅いコピー」であるという点です。


# dupメソッド
origin_arr = ["hoge", "fuga"]
=> ["hoge", "fuga"]

dup_arr = origin_arr.dup
=> ["hoge", "fuga"]

dup_arr.first.gsub!("hoge", "piyo")
=> "piyo"

dup_arr
=> ["piyo", "fuga"]

origin_arr
=> ["piyo", "fuga"]

# cloneメソッド
clone_arr = origin_arr.clone
=> ["piyo", "fuga"]

clone_arr.first.gsub!("piyo","hoge")
=> "hoge"

clone_arr
=> ["hoge", "fuga"]

origin_arr
=> ["hoge", "fuga"]

上記のように、浅いコピーによって生成したオブジェクトがhashやarrayなど階層構造になっている場合、この中身に対して破壊的メソッドを実行すると元のオブジェクトの中身にも変更が及んでしまいます。危険だ。

origin_arr.equal? dup_arr
=> false

origin_arr[0].equal? dup_arr[0]
=> true

配列そのものは別オブジェクトとして生成されていますが、その中身は共通になっていることがわかります。リファレンスにもそう書いてある。

clone や dup はオブジェクト自身を複製するだけで、オブジェクトの指している先(たとえば配列の要素など)までは複製しません。

深いコピーを実現するためには

人間生きていれば深いコピー、つまりオリジナルと分離した完全なる複製を生成したい時がある。そういうときにどうしたらいいか。以下の3つの方法があるようです。

1. deep_dup メソッドを使う(要 activesupport)

深いコピーを複製してくれるメソッドです。これはrubyの標準ライブラリにはないメソッドなので、activesupportをrequireしないと使えないことに留意してください。


require 'active_support'
=> true

require 'active_support/core_ext'
=> true

deep_dup_arr = origin_arr.deep_dup
=> ["hoge", "fuga"]

deep_dup_arr.first.gsub!("hoge", "piyo")
=> "piyo"

origin_arr
=> ["hoge", "fuga"]

deep_dup_arr
=> ["piyo", "fuga"]

2.initialize_copy メソッドを定義して浅いコピーを2重に行う

例えば以下のようなクラスがあったとします。


class Profile
  attr_accessor :name, :age
  def initialize(name, age)
    @name = name
    @age = age
  end
end

このクラスを呼び出すオブジェクトを定義し、浅いコピーで複製したら、中身は同じものになります。
そのため、複製したオブジェクトに対して破壊的メソッドを実行すると・・・


taro = Profile.new('taro', 20)
=> #<Profile:0x007f920a13b2c8 @age=20, @name="taro">

clone_taro = taro.clone
=> #<Profile:0x007f920ab86a20 @age=20, @name="taro">

clone_taro.name.replace("clone_taro")
=> "clone_taro"

taro.name
=> "clone_taro"

taroの名前がcloneになってしまいました。

これを解消できるメソッドがinitialize_copyです。
先程のProfileクラスにメソッドを追加して以下のようにします。


class Profile
  attr_accessor :name, :age
  def initialize(name, age)
    @name = name
    @age = age
  end

  def initialize_copy(orig)
    @name = orig.name.dup
  end
end

このinitialize_copyメソッドは、dupcloneメソッドを実行したときに自動的に呼び出されるメソッドです。
この中で再帰的にオブジェクトの中身に対して浅いコピーを実行する処理を入れ込むことで、

  • オブジェクトそのもの
    • オブジェクトの中身

と入れ子的に浅いコピーを複数回実行して中身も複製しようというものです。

taro = Profile.new('taro', 20)
=> #<Profile:0x007fba80bae9a0 @age=20, @name="taro">

clone_taro = taro.clone
=> #<Profile:0x007fba80fee100 @age=20, @name="taro">

clone_taro.name.replace("clone_taro")
=> "clone_taro"

taro
=> #<Profile:0x007fba80bae9a0 @age=20, @name="taro">

taro.nameは、taroのclone実行時にinitialize_copyメソッド内で自動的に再複製されます。そのためtaro.nameとclone_taro.nameが異なる中身となり、結果的に破壊的メソッドを実行してもオリジナルに変化はありません。

ただ、当然っちゃ当然なんですが、上記の例ではageの方はinitialize_copy内にdupの処理がないので、clone_taro.age.replace(1000000)などと実行するとオリジナルの方も変化します。replaceなどの置き換え処理が想定されるオブジェクトはすべてinitialize_copyに引いておく必要があります。
また、あくまで浅いコピーの中で浅いコピーをしているだけなので、さらにもう1段深い要素に対しての破壊的メソッドには無力です。前に型で判別させる(例えば is_a?(String)など)処理を入れておくとかすると良いのかなと思いました。


kirito = Profile.new(["kazuto", "kirigaya"], 14)
=> #<Profile:0x007fba819471c0 @age=14, @name=["kazuto", "kirigaya"]>

asuna = kirito.dup
=> #<Profile:0x007fba80bb7c58 @age=14, @name=["kazuto", "kirigaya"]>


# これは複製済みなのでオリジナルが保持される
asuna.name.replace(["asuna", "yuuki"])
=> ["asuna", "yuuki"]

kirito
=> #<Profile:0x007fba819471c0 @age=14, @name=["kazuto", "kirigaya"]>



# これはNG
kirito_sister = kirito.dup
=> #<Profile:0x007fba80d05330 @age=14, @name=["kazuto", "kirigaya"]>

kirito_sister.name.first.replace("suguha")
=> "suguha"

kirito
=> #<Profile:0x007fba819471c0 @age=14, @name=["suguha", "kirigaya"]>

3.Marshalクラスを使う

多次元配列などの深い要素に対して一時に完全な複製を生成したい、でもactivesupportは宗教上使えないし・・・という場合に使えるクラスです。
オブジェクトを文字列化したり、オブジェクト化した文字列を復元したりできます。
このクラスを使って対象のオブジェクトの文字列化→復元を経ると完全に違うものになります。そのため実質的に深いコピーを実現することが可能です。


original = ["hoge","fuga"]
=> ["hoge", "fuga"]

tmp = Marshal.dump(original)
=> "\x04\b[\aI\"\thoge\x06:\x06ETI\"\tfuga\x06;\x00T"

copy = Marshal.load(tmp)
=> ["hoge", "fuga"]

copy.first.replace("piyo")
=> "piyo"

copy
=> ["piyo", "fuga"]

original
=> ["hoge", "fuga"]

2のような場合もご覧のとおりです。


class Profile
  attr_accessor :name, :age
  def initialize(name, age)
    @name = name
    @age = age
  end
end

taro = Profile.new("taro", 14)
=> #<Profile:0x007fba80ee5e20 @age=14, @name="taro">

tmp = Marshal.dump(taro)
=> "\x04\bo:\fProfile\a:\n@nameI\"\ttaro\x06:\x06ET:\t@agei\x13"

clone_taro = Marshal.load(tmp)
=> #<Profile:0x007fba80e5f618 @age=14, @name="taro">

clone_taro.name.replace("clone_taro")
=> "clone_taro"

taro.name
=> "taro"

本来はファイルに書き出したりするのが主目的っぽいですが、
https://docs.ruby-lang.org/ja/latest/method/Object/i/clone.html
るりまにも記載のある手法なので、使って良さそうですね。

参考

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
86
Help us understand the problem. What are the problem?