about
shallow copy(浅いコピー)だdeep copy(深いコピー)だとよくわからないままrubyを使ってたらまんまと引っかかったので備忘録として書いてみる。間違っていたらご指摘ください
=
による複製
浅い・深いの関係ないところとして、=によってオブジェクトを複製すると両者は名前が違うだけの、同じ中身を参照している関係になります。
なので、破壊的変更を加えると複製元も先も変わります。
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"
dup
と clone
rubyにはオブジェクトの複製メソッドとして、dup
とclone
というメソッドが用意されています。
origin = "hoge"
=> "hoge"
copy = origin.dup
=> "hoge"
origin.object_id
=> 70118270776180
copy.object_id
=> 70118270801780
上記のように、object_idが異なるので、別物として作られていることがわかります。
そのため、この変数に破壊的メソッドを実行してもお互いに影響を及ぼすことはありません。
origin = "origin"
=> "origin"
cloned_origin = origin.clone
=> "origin"
cloned_origin.gsub!("origin", "clone")
=> "clone"
origin
=> "origin"
cloned_origin
=> "clone"
しかし、一つ気をつけなければならないのは、この2つのメソッドが生み出すコピーが「浅いコピー」であるという点です。
浅いコピーとは?
浅いコピー(shallow copy)とは、実体となる値の参照場所だけ複製して、値そのものは複製しないというコピー手法のことです。Rubyに限らず、JavaScriptといった他の言語でも採用している汎用的なパターンです。
例として、るりまにはこのように記載があります。
clone や dup はオブジェクト自身を複製するだけで、オブジェクトの指している先(たとえば配列の要素など)までは複製しません。これを浅いコピー(shallow copy)といいます。
# arrayの場合
origin_array = [1, 2, 3, 4, 5]
=> [1, 2, 3, 4, 5]
cloned_origin_array = origin_array.dup
=> [1, 2, 3, 4, 5]
origin_array.object_id
=> 299540
cloned_origin_array.object_id
=> 330260
origin_array[0].object_id
=> 3
cloned_origin_array[0].object_id
=> 3
# hashの場合
origin_hash = {a: 1, b: 2, c: 3}
=> {:a=>1, :b=>2, :c=>3}
cloned_origin_hash = origin_hash.clone
=> {:a=>1, :b=>2, :c=>3}
origin_hash.object_id
=> 563260
cloned_origin_hash.object_id
=> 603040
origin_hash[:a].object_id
=> 3
cloned_origin_hash[:a].object_id
=> 3
このように、arrayやhashのオブジェクトは別になっていますが、その中身は同じobject_idになっていることがわかります。
なので、
# 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"]
上記のように、dupやcloneメソッドを使ってarrayやhashを複製した場合、この中身に対して破壊的メソッドを実行すると元のオブジェクトの中身にも変更が及んでしまいます。危険だ。
深いコピーを実現するためには
人間生きていれば深いコピー、つまりオリジナルと分離した完全なる複製を生成したい時がある。そういうときにどうしたらいいか。以下の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
メソッドは、dup
やclone
メソッドを実行したときに自動的に呼び出されるメソッドです。
この中で再帰的にオブジェクトの中身に対して浅いコピーを実行する処理を入れ込むことで、
- オブジェクトそのもの
- オブジェクトの中身
と入れ子的に浅いコピーを複数回実行して中身も複製しようというものです。
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
るりまにも記載のある手法なので、使って良さそうですね。