Techioz Blog

Rubyでハッシュを複製する

概要

デフォルト値を持つ別のハッシュを使用して、Ruby でハッシュを初期化しようとしています。深いコピーが必要ですが、浅いコピーしか入手できないようです。

以下に例を示します。

DEFAULT_HASH = { a: 0, b: 1 }.freeze
my_hash = DEFAULT_HASH.dup
my_hash[:a] = 4 

現在、「my_hash」と DEFAULT_HASH の a の値は 4 です。変更したいのはハッシュの値だけです。

他のアプローチも試してみました。

my_hash = {}.merge DEFAULT_HASH

そして

my_hash.merge! DEFAULT_HASH

これらはすべて同じ効果をもたらします。この種の初期化を実現する最善の方法は何ですか。また、ネストされたハッシュも扱っているため、少し複雑になります。

つまり、私の DEFAULT_HASH は次のようになります。

DEFAULT_HASH = { a:{a:1, b:2}, b:{a:2, b:1} }

これはこれを行う方法に影響しますか?

編集: ネストされたハッシュ ケース

DEFAULT_HASH = { a:{a:1, b:2}, b:{a:2, b:1} }
=> {:a=>{:a=>1, :b=>2}, :b=>{:a=>2, :b=>1}} 
a=DEFAULT_HASH.dup
=> {:a=>{:a=>1, :b=>2}, :b=>{:a=>2, :b=>1}} 
a[:b][:a]=12
=> 12 
DEFAULT_HASH
=> {:a=>{:a=>1, :b=>2}, :b=>{:a=>12, :b=>1}} 

解決策

@pjs の指摘によれば、Hash#dup はハッシュの最上位レベルに対して「正しいことを行う」ということです。ただし、ネストされたハッシュの場合は、やはり失敗します。

gem の使用に抵抗がない場合は、(とりわけ) まさにこの目的のために私が作成した gem である deep_enumerable の使用を検討してください。

DEFAULT_HASH = { a:{a:1, b:2}, b:{a:2, b:1} }
dupped = DEFAULT_HASH.dup

dupped[:a][:a] = 'updated'

puts "dupped:       #{dupped.inspect}"
puts "DEFAULT_HASH: #{DEFAULT_HASH.inspect}"


require 'deep_enumerable'
DEFAULT_HASH = { a:{a:1, b:2}, b:{a:2, b:1} }

deep_dupped = DEFAULT_HASH.deep_dup
deep_dupped[:a][:a] = 'updated'

puts "deep_dupped:  #{deep_dupped.inspect}"
puts "DEFAULT_HASH: #{DEFAULT_HASH.inspect}"

出力:

dupped:       {:a=>{:a=>"updated", :b=>2}, :b=>{:a=>2, :b=>1}}
DEFAULT_HASH: {:a=>{:a=>"updated", :b=>2}, :b=>{:a=>2, :b=>1}}

deep_dupped:  {:a=>{:a=>"updated", :b=>2}, :b=>{:a=>2, :b=>1}}
DEFAULT_HASH: {:a=>{:a=>1, :b=>2}, :b=>{:a=>2, :b=>1}}

あるいは、次のようなことを試してみることもできます。

def deep_dup(h)
  Hash[h.map{|k, v| [k,
    if v.is_a?(Hash)
      deep_dup(v)
    else
      v.dup rescue v
    end
  ]}]
end

この最後の関数は deep_enumerable ほど十分にテストされていないことに注意してください。