ITエンジニアによるITエンジニアのためのブログ

Rubyの浅い(shallow)コピーと深い(deep)コピーの違い

Rubyでオブジェクトをコピーする際、浅いコピーと深いコピーの違いを理解していないと思わぬバグにつながりますので、両者の違いを例や表を使ってまとめてみました。

浅いコピー

オブジェクトがハッシュや配列等の複合型の場合、浅いコピーはオブジェクトネスト構造の一番外側のオブジェクトの複製を作成します。

文字列や整数などのスカラー値の場合、オブジェクトはネスト構造を持ちませんので、浅いコピーも深いコピーも一緒です。

以下の例で、ネストされたハッシュを使って解説します。

a = { foo: { bar: 'baz' } }
 => {:foo=>{:bar=>"baz"}} 
a.object_id
 => 180 
a[:foo].object_id
 => 200 
a[:foo][:bar].object_id
 => 220 

この状況を表に記載すると、以下のようになります。

変数オブジェクトIDオブジェクトの中身
a180{:foo=> 200参照 }
200{:bar=>220参照}
220“baz”

aという変数はIDが180のオブジェクトを参照しており、その値は:fooというキーを持つハッシュです。このfooというキーはIDが200のオブジェクトを参照しており、その中身は:barというキーを持つハッシュです。barというキーはIDが220のオブジェクトを参照しており、その中身はbazという文字列です。

次に上記オブジェクトの浅いコピーをdupメソッドを使って作成します。

b = a.dup
 => {:foo=>{:bar=>"baz"}} 
b.object_id
 => 240 
b[:foo].object_id
 => 200 
b[:foo][:bar].object_id
 => 220

表にまとめると以下のようになります。

変数オブジェクトIDオブジェクトの中身
a180{:foo=> 200参照 }
200{:bar=>220参照}
220“baz”
b240 {:foo=> 200参照 }

bはaとは違うオブジェクトIDを持ち、その値であるハッシュもaのハッシュとは別物ですが、bのハッシュ内のfooキーはaのハッシュのfooキーと同じ200番のオブジェクトを参照しています。

このように、浅いコピーはその名の通りオブジェクトの一番浅い箇所のコピーを作り、そのコピーは別のオブジェクトIDを持ちますが、オブジェクト内のプロパティが参照するオブジェクトは元のオブジェクトと一緒です。

aとbは別のオブジェクトなので、aのハッシュに要素を追加してもbのハッシュには影響ありません。

a[:hoge] = 'hogehoge'
 => "hogehoge"

a[:hoge].object_id
 => 260

a
 => {:foo=>{:bar=>"baz"}, :hoge=>"hogehoge"}

b
 => {:foo=>{:bar=>"baz"}} 
変数オブジェクトIDオブジェクトの中身
a180{:foo=> 200参照, :hoge=> 260参照 }
200{:bar=>220参照}
220“baz”
b240{:foo=> 200参照 }
260“hogehoge”

また、aの:fooキーに別のオブジェクトをアサインしても、bのfooの参照先には影響がありません。これはfooの元の参照先のオブジェクトを変更しているわけではなく、fooの参照先を変えているためです。

a[:foo] = 'barbar'
 => "barbar"

b
 => {:foo=>{:bar=>"baz"}}

変数オブジェクトIDオブジェクトの中身
a180{:foo=> 360参照, :hoge=> 260参照 }
200{:bar=>220参照}
220“baz”
b240{:foo=> 200参照 }
260“hogehoge”
360‘barbar’

但し、aとbで共通して参照しているオブジェクトを片方で変更すると、もう片方にも影響がでます。

# aの:fooキーが再び200番オブジェクトを参照するように修正
a[:foo] = b[:foo]
 => {:bar=>"baz"} 

# 200番オブジェクトの:barキーに新オブジェクトをアサイン
a[:foo][:bar] = 'BAZ'
 => "BAZ"

b
 => {:foo=>{:bar=>"BAZ"}} 

表にすると以下の通りです。

変数オブジェクトIDオブジェクトの中身
a180{:foo=> 200参照, :hoge=> 260参照 }
200{:bar=>220参照}
220“BAZ”
b240{:foo=> 200参照 }
260“hogehoge”
360‘barbar’

浅いコピーの問題点

例えば、以下のような状況を考えます。

・ActiveRecordのレコードの1つのカラムからハッシュを取り出し、元レコードに影響ないようにコピーし、コピーしたデータを加工した。

・一方、同じレコードの別カラムを編集したため、レコードを保存した。

仮コードにすると以下のようになります。

example = Example.find 1
example.data
=> {"foo"=>{"bar"=>"baz"}}

# 浅いコピー作成
copied_data = example.data.dup
copied_data['foo']['bar'] = 'bazbaz'

# 意図としては以下のカラムだけ変更して保存したかった。
example.another_column = 'hoge'
example.save

# 但し、exampleのdataカラムまで意図せずDBに保存されてしまう。
example.reload
example.data
=> {"foo"=>{"bar"=>"bazbaz"}}

これは、浅いコピーだとオリジナルオブジェクトとコピーされたオブジェクトが内部のデータを共有しているため、コピーされたオブジェクトから共有データを変更するとオリジナルのオブジェクトのデータも変化し、ActiveRecordの更新対象となるからです。

この問題を解決するには、浅いコピーではなく深いコピーを使います。

深いコピー

ハッシュや配列などの複合オブジェクトに対し深いコピーを使うと、オブジェクト自身だけでなく内包されているデータ全てのコピーが作成されます。

ruby自身には深いコピー作成用のメソッドがありませんが、Railsにはdeep_dupというメソッドが存在します。

a = { foo: { bar: 'baz' } }
=> {:foo=>{:bar=>"baz"}}

# aの深いコピーをRailsのdep_dupメソッドで作成
b = a.deep_dup
=> {:foo=>{:bar=>"baz"}}

一見すると浅いコピーと同じに見えますが、オブジェクトIDを確認すると違うことがわかります。

a.object_id
=> 32140

a[:foo].object_id
=> 32160

a[:foo][:bar].object_id
=> 32180

b.object_id
=> 32200

b[:foo].object_id
=> 32220

b[:foo][:bar].object_id
=> 32240

浅いコピーとは違い、深いコピーの場合はオブジェクト内のすべてのデータに対してコピーが作られ、それぞれ別のオブジェクトIDになっていることがわかります。

表にすると以下の通りです。

変数オブジェクトIDオブジェクトの中身
a32140{:foo=> 32160参照}
32160{:bar=>32180参照}
32180“baz”
b32200{:foo=> 32220参照 }
32220{:bar=>32240参照}
32240‘baz’

このため、コピーのどの部分を変更しても、元のオブジェクトには影響がありません。

b[:foo][:bar] = 'BAZ'
=> "BAZ"

a[:foo][:bar]
=> "baz"

浅いコピーの項目で例に使ったRailsのActiveRecord保存ミスも、深いコピーを使うことで防ぐことができます。

example = Example.find 1
example.data
=> {"foo"=>{"bar"=>"baz"}}

# 元データに影響がでないように深いコピー作成
copied_data = example.data.deep_dup
copied_data['foo']['bar'] = 'bazbaz'

# 以下のカラムだけ変更して保存したい。
example.another_column = 'hoge'
example.save

# 深いコピーのため、exampleのdataカラムは変更されておらず、DBへの保存対象にならない。
example.reload
example.data
=> {"foo"=>{"bar"=>"baz"}}

ただし、常に深いコピーを使うべきかというとそうではありません。浅いコピーの方がメモリ効率は良いですし、深いコピーを使うとオブジェクト間のデータ共有が逆にできなくなりますので、本当に必要なときにだけ深いコピー使うべきでしょう。

まとめ

浅いコピーはオブジェクト構造の一番浅い階層のみのコピーを作成するのに対し、深いコピーはオブジェクトの全ての階層のコピーを作成します。意図しないデータ変更ミスを防ぐためにも、それぞれの特徴を理解し、場面に応じた適切なコピー手法を用いることが重要です。