144
89

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2018-06-11

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が異なるので、別物として作られていることがわかります。
そのため、この変数に破壊的メソッドを実行してもお互いに影響を及ぼすことはありません。

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メソッドは、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
るりまにも記載のある手法なので、使って良さそうですね。

参考

144
89
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
144
89

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?