ときおりハッシュや配列のディープコピーが必要になりますが、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から起こしたような、ハッシュ・配列の組み合わせ構造の場合に限られるようです。