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 defaul开发者_运维百科t'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"=>[]}
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.
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
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"=>[]}
A way to do this is as follows:
ary = Marshal.load(Marshal.dump(default))
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"=>[]}
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.
精彩评论