Rubyの変数の振る舞いまとめ

下のQiitaの記事にコメントしたのだけど、なかなか整理するのが難しかったので、記事にすることにした。

- [新人エンジニアがはまるかもしれないrubyの浅いコピーと深いコピーともろもろ背景 - Qiita](http://qiita.com/sue738/items/92228f0ae0518bf0a693)

Rubyの変数の振る舞いは難しいようで、理屈がわかってしまえばシンプルな理屈だ。ちょっと長くなってしまったが自分なりに整理してみた。

## いくつかの例

Rubyの変数の振る舞いをいくつか見てみよう。まずは配列をコピーしてそれを逆順にしてみたい。

```test1.rb
a = ['a', 'b', 'c']
b = a
b.reverse!

p b
# => ['c', 'b', 'a']
p a
# => ['c', 'b', 'a']
```

`b`に`a`のオブジェクトを代入したのだが、`b`のオブジェクトを変更すると`a`のオブジェクトも変更されてしまった。では、`Object#dup`を使用してオブジェクトをコピーしてみよう。

```test2.rb
a = ['a', 'b', 'c']
b = a.dup
b.reverse!

p b
# => ['c', 'b', 'a']
p a
# => ['a' ,'b' ,'c']
```

なるほど、今度は変数`b`のオブジェクトを変更しても変数`a`のオブジェクトは変更されなかった。`Object#dup`を使えばオブジェクトは複製できるようなので、今度は値を`Hash`にしてみよう。ついでにキーの値を変更したい。

```test3.rb
a = [{item: 'val1'}, {item: 'val2'}, {item: 'val3'}]
b = a.dup
b[0][:item] = 'new value'
b.reverse!

p b
# => [{:item=>"val3"}, {:item=>"val2"}, {:item=>"new value"}]
p a
# => [{:item=>"new value"}, {:item=>"val2"}, {:item=>"val3"}]
```

変数`a`は逆順にはなっていないが、変数`a`の配列の要素の`Hash`の値も書き換わってしまった。

## なにが起こっているのか

だいたいの人はこの時点で気がつくので解説は要らないのであるが、一応自分なりに説明してみよう。

Rubyの変数はオブジェクトの実体そのものではなく、実体を参照するラベルのようなものである。つまり、変数`a`にオブジェクトを代入する行為は、変数`a`がオブジェクトを参照するようにすることである。変数`b`を変数`a`に代入すると、変数`b`も変数`a`も同じオブジェクトを参照するようになる。Rubyのオブジェクトはすべて固有のidをもっているので、これを見てみればよい。

```test4.rb
a = ['a', 'b', 'c']
b = a

p b.object_id
# => 70196368118500
p a.object_id
# => 70196368118500
```

オブジェクトを複製するには、`Object#clone`を使えば良い。しかし、このとき行われるのはオブジェクト自体のコピーだけであるので、オブジェクトのインスタンス変数が他のオブジェクトを参照している場合、その参照もそのままコピーされる。

```test5.rb
a = [{item: 'val1'}, {item: 'val2'}, {item: 'val3'}]
b = a.clone

# 複製されたオブジェクトは異なるidを持っている
p b.object_id
# => 70196368297280
p a.object_id
# => 70196368278180

# 参照はそのままコピーされるので、配列の値は同じオブジェクトを参照している。
p b[0].object_id
# => 70196368278280
p a[0].object_id
# => 70196368278280
```

この挙動は浅いコピー(shallow copy)と呼ばれる。そもそも`Object#clone`のリファレンスマニュアルをちゃんと読んでいればこの罠にはまることは少ないはずだ。

- [instance method Object#clone](http://docs.ruby-lang.org/ja/2.1.0/method/Object/i/clone.html)

すべての値を複製するためには、オブジェクトが参照しているオブジェクト、参照しているオブジェクトが参照している全てのオブジェクト、参照しているオブジェクトが参照しているオブジェクト………といったかんじで複製していく必要がある。これは深いコピー(deep copy)と呼ばれる。

Rubyでは深いコピーを行う機能は提供されていない。これを簡単に実現する方法がオブジェクトのシリアライズ/デシリアライズである。シリアライズ、デシリアライズとはオブジェクトを文字列に変換、そして復元することである。Rubyでは、`Marshal`モジュールでオブジェクトをシリアライズ/デシリアライズする機能が提供されている。

```rb
b = Marshal.load(Marshal.dump(a))
```

`Marshal`を使えば、オブジェクトは一旦すべて文字列に変換され、その文字列をもとに新しいオブジェクトを生成するため、すべての値が複製されたのと同じ効果を得られる。もちろん`Marshal`が使用できないオブジェクトはこの方法で複製できないので注意が必要である。

## コピーの罠にはまらないために

さて、そもそもなぜこんな罠にはまったのかを考えてみよう。それは破壊的操作を行うメソッドによりオブジェクト自体を変更したからだ。破壊的操作とはその名の通りオブジェクト自体に変更を加える。もちろん、このときオブジェクトのidは変更されない。

```test6.rb
a = ['a', 'b', 'c']

p a.object_id
# => 70196368230840

a.reverse!

p b.object_id
# => 70196368230840
```

では、破壊的操作を行わないメソッドを使用していればどうだっただろうか。メソッドが実行された時点で新しいオブジェクトが生成される。変数`b`は新しいオブジェクトを参照することになる。変更された時点で新しいオブジェクトが生成されるため、無駄なリソースなども消費しない。

```test7.rb
a = ['a', 'b', 'c']
b = a                # この時点ではまだbもaも同じオブジェクトを参照している
b = b.reverse        # ここでbは新しいオブジェクトを参照する

p b
# => ['c', 'b', 'a']
p a
# => ['a', 'b', 'c']
```

このように、破壊的操作を行うときにオブジェクトを複製すればよい。`Array#[]=`、`Hash#[]=`も破壊的操作に該当するので、そのまえにオブジェクトを複製する。

```test8.rb
a = [{item: 'val1'}, {item: 'val2'}, {item: 'val3'}]
b = a
b = b.reverse
b[0] = b[0].clone.tap{|h| h[:item] = 'new value' } # 複製してから修正

p b
# => [{:item=>"new value"}, {:item=>"val2"}, {:item=>"val1"}]
p a
# => [{:item=>"val1"}, {:item=>"val2"}, {:item=>"val3"}]
```

このように`b[0]`の参照しているオブジェクトを複製して、そのオブジェクトに対して破壊的操作を行えば、`a`の参照しているオブジェクトの参照しているオブジェクトには影響しない。`Hash#merge`を使った方がシンプルかもしれない。

```test9.rb
a = [{item: 'val1'}, {item: 'val2'}, {item: 'val3'}]
b = a
b = b.reverse
b[0] = b[0].merge({item: 'new value'}) # mergeは非破壊

p b
# => [{:item=>"new value"}, {:item=>"val2"}, {:item=>"val1"}]
p a
# => [{:item=>"val1"}, {:item=>"val2"}, {:item=>"val3"}]
```

ということで、コピーの罠だと思っていたのは破壊的操作の罠だったといえるかもしれない。破壊的操作が行われて困る場合は`Object#freeze`を使うのも手だ。

## まとめ

この変数とコピーの挙動は、あれどうだったっけとなることがたまにある。しかし、そういうときは大抵配列とハッシュが入れ子になった巨大な構造を変更する場合など、そもそもプログラムの構造が悪そうなときであることが多い。

Rubyらしいコードを書くように気をつければ、おのずと`Object#clone`のようなメソッドを使う機会も減っていくのではないだろうか。その頃にはこういう罠にはまることもなさそうだけど。