ときおりハッシュや配列のディープコピーが必要になりますが、Rails標準で機能が用意してありました。
変数のコピー、シャローコピー
Rubyはオブジェクト指向の言語ですが、変数はオブジェクトへの参照となっているので、変数をコピーしても、オブジェクト自体は1つのままです。
a = { foo: :bar }
b = a
b[:hoge] = :piyo
# 共通のオブジェクトなので、aも変わる
p a[:hoge] # => :piyo
そして、オブジェクトをコピーするためのメソッドとしてObject#dup
があります。これを使えば上のようなことはなくなりますが、まだ完全とはいい難いです。
a = { foo: [1, 2, 3] }
# ハッシュのコピーを作る
b = a.dup
b[:hoge] = :piyo
# ハッシュは別になっているので、aには影響しない
p a[:hoge] # => nil
# a[:foo]の配列は共通のまま
b[:foo] << 4
p a[:foo].length # => 4
ということで、#dup
でコピーされるのは自分自身のオブジェクトだけで、このような1階層のコピーをシャローコピー(shallow = 浅い)といいます。
ディープコピーするには~Ruby標準で~
それでは、中身までコピーするディープコピーを行うにはどうすればいいでしょうか。プレーンなRubyでは、いったんRubyのデータ構造を文字列にエンコード・デコードするMarshal
モジュールを使って、以下のように書くというイディオムがあります。
b = Marshal.load(Marshal.dump(a))
これでも動くといえば動くのですが、ちょっと間接的というか、まどろっこしい感じがします。
Railsだともっとシンプル
Ruby on Rails(のActiveSupport)では、もっと単純な方法で実行できます。
b = a.deep_dup
実装内容と制限
これがどのように実装されているか確認してみたところ、core_ext/object/deep_dup.rb
という1ファイルになっていました。
-
dup
不可能なもの(整数、true
、false
、nil
、Symbol
など)…そのまま -
Array
…各要素に#deep_dup
を再帰適用してmap
-
Hash
…まずハッシュ全体をdup
して、(キーのdup
が必要なら行いつつ)値を#deep_dup
- それ以外…単純に
dup
ということで、「Array
とHash
以外のデータ構造」の場合は、自分で#deep_dup
を実装しないと正しく実行できません。あと、再帰的なデータ構造を作り上げてしまった場合(Array
の要素に自分自身を入れた場合など)、無限ループしてしまいます。#deep_dup
が便利なのは、ちょうどJSONから起こしたような、ハッシュ・配列の組み合わせ構造の場合に限られるようです。