RubyのオブジェクトIDと、ディープ/シャロー(deep/shallow)コピーについて

更新がお久しぶりとなってしまいました。
今回は、Rubyを1年くらい業務で使っていて、実は知らなかった面白いRubyのことについて書きました。
ディープコピーとシャローコピーについてです。

RubyはすべてにオブジェクトIDがあるんです。
オブジェクトIDはObject#object_idもしくはBasicObject#__id__で調べることができます。

BasicObjectの説明にあるように、

通常のクラスは Object またはその他の適切なクラスから派生すべきです。 真に必要な場合にだけ BasicObject から派生してください。

ということなので、Object#object_idを使おうと思います。
(masutakaさん、コメントありがとうございました)

1
2
a = '1'
a.object_id #=> 70183688335800

すべてと書いたのは、変数以外にも使えるということです。
なので、Fixnumである1とか、文字列である'1'とかにもオブジェクトIDがあります。

1
2
1.object_id #=> 3
'1'.object_id #=> 70183691563540

ちなみに、'1'とかは、内部的にはString.new('1')をしているのと同じなので、毎回オブジェクトIDが異なります。
ただ1は、オブジェクトIDは変わりません。

1
2
3
4
5
6
7
'1'.object_id #=> 70183688337580
'1'.object_id #=> 70183688223040
'1'.object_id #=> 70183696114180
1.object_id #=> 3
1.object_id #=> 3
1.object_id #=> 3

(なんかオブジェクトIDの番号の付け方にも法則がありそうですね)


【追記】

ただし、'1'に対して``Object#freezeを使うと、オブジェクトIDは変わらないそうです。

1
2
3
4
5
'1'.object_id #=> 70305889404440
'1'.object_id #=> 70305897175740
'1'.freeze.object_id #=> 70305889126160
'1'.freeze.object_id #=> 70305889126160(変わってない!)

で、実はRubyの代入である=って、このオブジェクトIDも一緒にコピーするんですね。
なので、コピーしたあとに、破壊的メソッドで値を変更すると、コピー元のオブジェクトにも変更が加えられてしまいます。

1
2
3
4
5
6
7
8
9
10
11
original = 'a'
copy = original
copy #=> 'a'
original.object_id #=> 70183687927560
copy.object_id #=> 70183687927560
original.gsub!(/a/,'c')
original #=> 'c'
copy #=> 'c'(変わってる…!)

これを防ぐには#dupを使います。

1
2
3
4
5
6
7
8
9
original = 'a'
copy = original.dup
original.object_id #=> 70115509211720
copy.object_id #=> 70115508963680(オブジェクトIDが違う…!)
original.gsub!(/a/,'c')
original #=> 'c'
copy #=> 'a'(変わってない…!)

先ほどの例のように、破壊的メソッドとかを使う場合で、他の変数の値を変えたくない場合とかに使えるかと思います。
ただ、この#dupはシャローコピーと呼ばれていて、浅いコピーとなっています。
例えば、配列のコピーだと、一次元配列までしか違うオブジェクトidにならないんです。

1
2
3
4
5
6
7
8
original = ['a',['b']]
copy = original.dup
original.object_id #=> 70115492272260
copy.object_id #=> 70115493561860
original[0].object_id #=> 70115492272420
copy[0].object_id #=> 70115492272420

なので、多次元配列(配列の要素の中に配列がある)で、2次元以上の場所を指定して破壊的メソッドを使ってしまうと…?

1
2
3
4
5
6
7
8
9
10
original = ['a',['b']]
copy = original.dup
original.delete('a')
original #=> [['b']]
copy #=> ['a', ['b']](変わっていない!)
original[0].delete('b')
original #=> [[]]
copy #=> ["a", []](削除されている!)

多次元配列で、2次元以上の要素も変更したくないのであれば、Marshalクラスのメソッドを使います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
original = [['a'],['b']]
tmp = Marshal.dump(original)
copy = Marshal.load(tmp)
original.object_id #=> 70115509109460
copy.object_id #=> 70115496815740
original[0].object_id #=> 70115509109520
copy[0].object_id #=> 70115496815720
original[0].delete('a')
original #=> [[], ['b']]
copy #=> [['a'], ['b']](変わってない!)

どれだけ深くても大丈夫です!

1
2
3
4
5
6
7
8
9
original = [[['a']],'b']
tmp = Marshal.dump(original)
copy = Marshal.load(tmp)
original[0][0].delete('a')
original #=> [[[]], 'b']
copy #=> [[['a']], 'b'](変わってない!)

この、配列の深さに関係なく全く別のオブジェクトIDをコピーすることをディープコピーといいます。
破壊的メソッドを使わないといけない場面で、予期せぬところで変更されてしまった場合にはシャローコピーとかディープコピーで全く違うオブジェクトを生成すると良いと思います!
(rspecとかで使えるかも?)

ただ、多用すると多分あんまり良くないのかな?
その辺詳しい方教えてください!