Dupliquer un Hash ou un Array en profondeur, c’est à dire en dupliquant l’ensemble des clés, et sous-clés n’est pas forcément trivial. Par exemple la méthode dup (ou clone) permet de le faire mais uniquement au premier niveau. Si on veut une recopie complète il n’y a pas de méthode dans les classes Hash et Array. On peut pour ça utiliser Marshal) pour sérialiser les objets et les recharger dans un nouvel objet. Cette méthode (simple) a des limites :

  • Tous les objets ne peuvent pas être sérialisés (je cite la doc de Marshal pour ruby 1.9.3 : Some objects cannot be dumped: if the objects to be dumped include bindings, procedure or method objects, instances of class IO, or singleton objects, a TypeError will be raised.)
  • L’exemple ci-dessous montre un objet hash utilisant une procédure par défaut qui va déclencher une exception: can't dump hash with default proc

Si toutefois votre objet Hash ou Array n’a pas plus de deux niveaux, et que vous avez besoin d’itérer sur des objets plus ou moins conséquents en taille, il est intéressant d’utiliser une autre méthode de duplication comme dans l’exemple ci-dessous. On peut noter un rapport de quasiment 4 sur le temps d’exécution entre Marshal et une duplication classique.

class MyObj

    def initialize(value)
        @value = value
    end

    def to_s
        @value.to_s
    end

    def set(value)
        @value = value
    end
    

end

class Hash

    def my_dup
        Marshal.load( Marshal.dump(self))
    end

end

puts "Hash duplication"
h = Hash.new 

h[:entry] = MyObj.new(5)
h[:hash] = Hash.new
h[:hash][:subhash] = MyObj.new(100)
d = Marshal.load( Marshal.dump(h) )

d[:entry].set(12)
h[:hash][:subhash].set(999)

puts "  h[:entry]:#{h[:entry]}(#{h[:entry].class})"
puts "  d[:entry]:#{d[:entry]}(#{d[:entry].class})"
puts "  h[:hash][:subhash]:#{h[:hash][:subhash]}(#{h[:hash][:subhash].class})"
puts "  d[:hash][:subhash]:#{d[:hash][:subhash]}(#{d[:hash][:subhash].class})"

puts "Array duplication"
a = [ 1 , 3 , MyObj.new(22) ]
ad = Marshal.load( Marshal.dump(a))

ad[2].set(29)

3.times do |value|
    puts "  a[#{value}]:#{a[value]}(#{a[value].class})"
    puts "  ad[#{value}]:#{ad[value]}(#{ad[value].class})"
end

puts "Exception raised due to default proc"
h = Hash.new {|hash,key| hash[key] = 0}

begin
    d = Marshal.load( Marshal.dump(h) )
rescue => e
    puts "  Error => #{e}"
end

puts "Use Marshal with my_dup method"
h = Hash.new
h[:entry_new] = MyObj.new(15)
d = h.my_dup
d[:entry_new].set(25)
puts "  h[:entry_new]:#{h[:entry_new]}(#{h[:entry_new].class})"
puts "  d[:entry_new]:#{d[:entry_new]}(#{d[:entry_new].class})"


puts "Duplicate with 2 levels only"
h = Hash.new
h[:entry_new] = MyObj.new(150)
d= {}
h.each {|key,value| d[key] = value.clone rescue value}
d[:entry_new].set(250)
puts "  h[:entry_new]:#{h[:entry_new]}(#{h[:entry_new].class})"
puts "  d[:entry_new]:#{d[:entry_new]}(#{d[:entry_new].class})"


puts "Benchmark Marshal vs dup with 2 levels"

h = Hash.new

10000.times {|i| h[i] = MyObj.new(i)}
n = 1000
Benchmark.bm(17) do |x|
  x.report("  Marshal     :") { n.times do ; d = Marshal.load( Marshal.dump(h) ) ; end }
  x.report("  Dup 2 levels:") { n.times do ; h.each {|key,value| d[key] = value.clone rescue value} ; end }
Hash duplication
  h[:entry]:5(MyObj)
  d[:entry]:12(MyObj)
  h[:hash][:subhash]:999(MyObj)
  d[:hash][:subhash]:100(MyObj)
Array duplication
  a[0]:1(Fixnum)
  ad[0]:1(Fixnum)
  a[1]:3(Fixnum)
  ad[1]:3(Fixnum)
  a[2]:22(MyObj)
  ad[2]:29(MyObj)
Exception raised due to default proc
  Error => can't dump hash with default proc
Use Marshal with my_dup method
  h[:entry_new]:15(MyObj)
  d[:entry_new]:25(MyObj)
Duplicate with 2 levels only
  h[:entry_new]:150(MyObj)
  d[:entry_new]:250(MyObj)
Benchmark Marshal vs dup with 2 levels
                        user     system      total        real
  Marshal     :    25.010000   0.010000  25.020000 ( 24.995725)
  Dup 2 levels:     6.500000   0.000000   6.500000 (  6.494289)

Un point à explorer est la méthode deep_dup pour les classes Object, Hash et Array disponible dans Rails.