先日新人エンジニアと話をしていてちょっと面白かった話。
rubyの代入って
a = 1
b = a
a = 2
ってやったらbの値はもちろん
b
=> 1
じゃないですか。一方で、
a = User.new
b = a
a.name = "sample"
ってやったらb.nameは
b.name
=> "sample"
となりますね。b.nameの値は変更してないのになんでsampleになったの?と。
なんでか
各変数のobject_idを見てみると分かりやすいです。
a = User.new
b = a
とした時点で、
a.object_id
=> 70352858783920
b.object_id
=> 70352858783920
同じ値なんですね。aのオブジェクトとbのオブジェクトは一緒。rubyは基本参照型なんですよね。aが参照してる先とbが参照してる先は一緒で、aが参照してる先の値を帰ればbが参照してる先の値ももちろん変わります。これを浅いコピーと言います。←[追記]これは浅いコピーとは言いませんでした。浅いコピーは本記事下部にて説明します。
a=2の例で値が変わらなかったのはなんで?
こんな疑問が上がってきました。
これもobject_idの値を見てみると分かりやすいです。
a = 1
b = a
とした時点で、
a.object_id
=> 3
b.object_id
=> 3
object_idは一緒です。ここで、
a = 2
としました。このとき、
a.object_id
=> 5
と切り替わったんですね。でもbがaのオブジェクトを参照しているのであれば、bの値も2になるんじゃないの?ってなるかもしれないですが、bが参照してるのはaのオブジェクトじゃないんですね。
1.objec_id
=> 3
2.object_id
=> 5
つまり、最初にa=1
の時点でaは「1」のオブジェクトを参照してて、b=a
した時点でbが参照したのはaが参照していた「1」のオブジェクト。ここでa=2
としても、bが指してるのはaのオブジェクトじゃなくて「1」のオブジェクトなのでaを変更してもbの値は変わらないと。
userオブジェクトをコピーして別のものとして使いたい
さて長くなりましたが本題で、userオブジェクトをコピーして全く別のものとして使いたいというのがこれ書いたきっかけでした。
その場合、Marshalを使います。
a = User.new
b = Marshal.load(Marshal.dump(a))
これでbにはaのデータがそのままコピーされて、aの値を変化してもbには影響しないオブジェクトを作成できました。これを深いコピーと言います。
a.object_id
=> 70352782085540
b.object_id
=> 70352819525260
object_idも変わりました。
[追記]浅いコピーと深いコピーについて
浅いコピーと深いコピーについてコメントいただいた@harukasanさんが詳しくまとめてくれました。
http://qiita.com/harukasan/items/80523509b446b5033ba9
簡単に言うと、浅いコピーも深いコピーもオブジェクトを複製してobject_idの異なるオブジェクトを作ります。ただ、浅いコピーの場合はコピーしたオブジェクトが配列で、中にさらにオブジェクトを持つ場合に、その要素は複製しないんですね。値を変えると元のオブジェクトも変更されてしまいます。
浅いコピー
a = [User.new, User.new]
b = a.dup
# aとbのobject_idは異なる。
a.object_id
=> 70249309167760
b.object_id
=> 70249318096940
# aとbの要素のobject_idは一緒。なので値を変えると相互に影響する。
a[0].object_id
=> 70249265135020
b[0].object_id
=> 70249265135020
深いコピー
a = [User.new, User.new]
b = Marshal.load(Marshal.dump(a))
# aとbのobject_idは異なる。
a.object_id
=> 70249318635740
b.object_id
=> 70249318761240
# aとbの要素のobject_idも異なる
a[0].object_id
=> 70249318618160
b[0].object_id
=> 70249318761200
まとめると、それぞれの参照、コピーの仕方で下記覚えておくと良いと思います。
# ただの参照
b = a
# 浅いコピー
b = a.dup
# 深いコピー
b = Marshal.load(Marshal.dump(a))