Jeremy Smith
Jeremy Smith

Reputation: 15069

How do I use default on a hash of empty arrays?

I want to use default to reset my ary when I need to. But I can't figure out how to not have default's values changed when ary's values change.

> default = {"a"=>[], "b"=>[], "c"=>[]}
=> {"a"=>[], "b"=>[], "c"=>[]} 

> ary = default.clone
=> {"a"=>[], "b"=>[], "c"=>[]} 

> ary["a"] << "foo"
=> ["foo"] 

> default
=> {"a"=>["foo"], "b"=>[], "c"=>[]} 

Upvotes: 6

Views: 174

Answers (6)

tadman
tadman

Reputation: 211600

What you've discovered here is that Hash#clone only does a shallow clone, that is it only replicates itself but not the objects that are referenced within it.

There are a number of "deep clone" gems that address this specific problem, or you can write your own to work around it:

class Hash
  def deep_clone
    Hash[collect { |k,v| [ k, v.respond_to?(:deep_clone) ? v.deep_clone : v ] }]
  end
end

class Array
  def deep_clone
    collect { |v| v.respond_to?(:deep_clone) ? v.deep_clone : v }
  end
end

This will let you clone arbitrary Hash and Array objects as required.

Upvotes: 7

matt
matt

Reputation: 79743

Depending on what you want to do, a simpler alternative to writing a deep clone method might be to write a method that creates a new default array every time it's called:

def default
  {"a"=>[], "b"=>[], "c"=>[]}
end

ary = default #=> {"a"=>[], "b"=>[], "c"=>[]}

ary["a"] << "foo" #=> {"a"=>["foo"], "b"=>[], "c"=>[]}

default #=> {"a"=>[], "b"=>[], "c"=>[]}

Of course, if the contents of your default hash changes over the course of the program this won't work and you'll have to look into the cloning or marshalling techniques, but if the contents are fixed this might be a more straightforward solution.

Upvotes: 2

Dylan Markow
Dylan Markow

Reputation: 124419

clone only does shallow copies, which is why cloning your hash still keeps everything pointed at the same nested arrays.

You can avoid this through the Marshal class by dumping and then loading in the object values:

> default = {"a" => [], "b" => [], "c" => []}
=> {"a"=>[], "b"=>[], "c"=>[]} 
> ary = Marshal.load(Marshal.dump(default))
=> {"a"=>[], "b"=>[], "c"=>[]} 
> ary["a"] << "foo"
=> ["foo"]
> default
=> {"a"=>[], "b"=>[], "c"=>[]} 

Upvotes: 2

Sujoy Gupta
Sujoy Gupta

Reputation: 1474

A way to do this is as follows:

ary = Marshal.load(Marshal.dump(default)) 

Upvotes: 2

user634545
user634545

Reputation: 9419

class Object
  def deep_clone
    Marshal::load(Marshal.dump(self))
  end
end

default = {"a"=>[], "b"=>[], "c"=>[]}
ary = default.deep_clone
ary["a"] << "foo"
default {"a"=>[], "b"=>[], "c"=>[]}

Upvotes: 2

Jaap Haagmans
Jaap Haagmans

Reputation: 6352

Both clone and dup create a shallow copy of your object, which results in this behaviour. I'm not sure what the proper way to achieve a deep copy is, but instead of:

ary = default.clone

Try:

ary = Marshal.load(Marshal.dump(default))

This is taken from a live 2.3.8 environment on ruby 1.8.7

Upvotes: 3

Related Questions