2

Im trying to clone a hash, to make a new copy of the original hash but it seems that when I set a value in the new hash, I have the same effect on the original hash.

rr = Hash.new
command = "/usr/local/bin/aws route53 list-resource-record-sets --hosted-zone-id EXAMPLEID --max-items 1"

rr=JSON.parse(%x{#{command}})

puts rr

if rr["ResourceRecordSets"][0]["TTL"] != 60
  new_rr = rr.clone
  new_rr["ResourceRecordSets"][0]["TTL"] = 60
  puts rr
  puts new_rr
end

Output:

{"NextRecordType"=>"MX", "NextRecordName"=>"example.com.", "ResourceRecordSets"=>[{"ResourceRecords"=>[{"Value"=>"1.2.3.4"}], "Type"=>"A", "Name"=>"example.com.", "TTL"=>1800}], "MaxItems"=>"1", "IsTruncated"=>true}
{"NextRecordType"=>"MX", "NextRecordName"=>"example.com.", "ResourceRecordSets"=>[{"ResourceRecords"=>[{"Value"=>"1.2.3.4"}], "Type"=>"A", "Name"=>"example.com.", "TTL"=>60}], "MaxItems"=>"1", "IsTruncated"=>true}
{"NextRecordType"=>"MX", "NextRecordName"=>"example.com.", "ResourceRecordSets"=>[{"ResourceRecords"=>[{"Value"=>"1.2.3.4"}], "Type"=>"A", "Name"=>"example.com.", "TTL"=>60}], "MaxItems"=>"1", "IsTruncated"=>true}

I dont see Hash.clone documented in Ruby 2.0, should I be using another method to create a Hash copy now?

Thanks in advance.

UKCC
  • 70
  • 7
  • Your hash has a nested structure. So, try to use deep cloning hack: http://stackoverflow.com/questions/8206523/how-to-create-a-deep-copy-of-an-object-in-ruby – Yevgeniy Anfilofyev Dec 03 '13 at 15:01

2 Answers2

4

Hash is a collection of keys and values, where values are references to objects. When duplicating a hash, new hash is being created, but all object references are being copied, so as result you get new hash containing the same values. That is why this will work:

hash = {1 => 'Some string'} #Strings are mutable
hash2 = hash.clone

hash2[1] #=> 'Some string'
hash2[1].upcase!            # modifying mutual object
hash[1] #=> 'SOME STRING;   # so it appears modified on both hashes
hash2[1] = 'Other string'   # changing reference on second hash to another object
hash[1] #=> 'SOME STRING'   # original obejct has not been changed

hash2[2] = 'new value'      # adding obejct to original hash
hash[2] #=> nil

If you want duplicate the referenced objects, you need to perform deep duplication. It is added in rails (activesupport gem) as deep_dup method. If you are not using rails and don;t want to install the gem, you can write it like:

class Hash
  def deep_dup
    Hash[map {|key, value| [key, value.respond_to?(:deep_dup) ? value.deep_dup : begin 
        value.dup
      rescue
        value
      end]}]
  end
end

hash = {1 => 'Some string'} #Strings are mutable
hash2 = hash.deep_dup

hash2[1] #=> 'Some string'
hash2[1].upcase!            # modifying referenced object
hash2[1] #=> 'SOME STRING'
hash[1] #=> 'Some string;   # now other hash point to original object's clone

You probably should write something similar for arrays. I would also thought about writing it for whole enumerable module, but it might be slightly trickier.

vgoff
  • 10,980
  • 3
  • 38
  • 56
BroiSatse
  • 44,031
  • 8
  • 61
  • 86
1

The easiest way to make a deep copy of most Ruby objects (including strings, arrays, hashes and combinations thereof) is to use Marshal:

def deep_copy(obj)
  Marshal.load(Marshal.dump(obj))
end

For example,

h        = {a: 1, b: [:c, d: {e: 4}]} # => {:a=>1, :b=>[:c, {:d=>{:e=>4}}]}
hclone   = h.clone
hdup     = h.dup
hmarshal = deep_copy(h)

h[:b][1][:d][:e] = 5
h        # => {:a=>1, :b=>[:c, {:d=>{:e=>5}}]}
hclone   # => {:a=>1, :b=>[:c, {:d=>{:e=>5}}]}
hdup     # => {:a=>1, :b=>[:c, {:d=>{:e=>5}}]}
hmarshal # => {:a=>1, :b=>[:c, {:d=>{:e=>4}}]}
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100