Comment copier un hachage dans Ruby?

197

J'admets que je suis un peu un débutant rubis (écrit des scripts de râteau, maintenant). Dans la plupart des langues, les constructeurs de copie sont faciles à trouver. Une demi-heure de recherche ne l'a pas trouvée en rubis. Je souhaite créer une copie du hachage afin de pouvoir le modifier sans affecter l'instance d'origine.

Quelques méthodes attendues qui ne fonctionnent pas comme prévu:

h0 = {  "John"=>"Adams","Thomas"=>"Jefferson","Johny"=>"Appleseed"}
h1=Hash.new(h0)
h2=h1.to_hash

En attendant, j'ai eu recours à cette solution de contournement inélégante

def copyhash(inputhash)
  h = Hash.new
  inputhash.each do |pair|
    h.store(pair[0], pair[1])
  end
  return h
end
Abrupt
la source
Si vous avez affaire à des Hashobjets simples , la réponse fournie est bonne. Si vous traitez avec des objets de type Hash provenant d'endroits que vous ne contrôlez pas, vous devez déterminer si vous souhaitez que la classe singleton associée au Hash soit dupliquée ou non. Voir stackoverflow.com/questions/10183370/…
Sim

Réponses:

223

La cloneméthode est la méthode standard et intégrée de Ruby pour effectuer une copie superficielle :

irb(main):003:0> h0 = {"John" => "Adams", "Thomas" => "Jefferson"}
=> {"John"=>"Adams", "Thomas"=>"Jefferson"}
irb(main):004:0> h1 = h0.clone
=> {"John"=>"Adams", "Thomas"=>"Jefferson"}
irb(main):005:0> h1["John"] = "Smith"
=> "Smith"
irb(main):006:0> h1
=> {"John"=>"Smith", "Thomas"=>"Jefferson"}
irb(main):007:0> h0
=> {"John"=>"Adams", "Thomas"=>"Jefferson"}

Notez que le comportement peut être remplacé:

Cette méthode peut avoir un comportement spécifique à la classe. Si c'est le cas, ce comportement sera documenté sous la #initialize_copyméthode de la classe.

Mark Rushakoff
la source
Clone est une méthode sur Object, BTW, donc tout y a accès. Voir les détails de l'API ici
Dylan Lacey
29
L'ajout d'un commentaire plus explicite ici pour ceux qui ne lisent pas d'autres réponses est une copie superficielle.
grumpasaurus
La documentation #initialize_copy ne semble pas exister pour Hash, même s'il existe un lien vers celle-ci sur la page doc Hash ruby-doc.org/core-1.9.3/Hash.html#method-i-initialize_copy
philwhln
14
Et pour les autres débutants Ruby, "copie superficielle" signifie que chaque objet en dessous du premier niveau est toujours une référence.
RobW
9
Notez que cela n'a pas fonctionné pour les hachages imbriqués pour moi (comme mentionné dans d'autres réponses). J'ai utilisé Marshal.load(Marshal.dump(h)).
bheeshmar
178

Comme d'autres l'ont souligné, le clonefera. Sachez clonequ'un hachage fait une copie superficielle. C'est-à-dire:

h1 = {:a => 'foo'} 
h2 = h1.clone
h1[:a] << 'bar'
p h2                # => {:a=>"foobar"}

Ce qui se passe, c'est que les références du hachage sont copiées, mais pas les objets auxquels elles font référence.

Si vous voulez une copie complète, alors:

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

h1 = {:a => 'foo'}
h2 = deep_copy(h1)
h1[:a] << 'bar'
p h2                # => {:a=>"foo"}

deep_copyfonctionne pour tout objet pouvant être rassemblé. La plupart des types de données intégrés (Array, Hash, String, etc.) peuvent être rassemblés.

Marshalling est le nom de Ruby pour la sérialisation . Avec le marshaling, l'objet - avec les objets auxquels il fait référence - est converti en une série d'octets; ces octets sont ensuite utilisés pour créer un autre objet comme l'original.

Wayne Conrad
la source
C'est bien que vous ayez fourni des informations sur la copie en profondeur, mais il devrait être accompagné d'un avertissement indiquant que cela peut provoquer des effets secondaires involontaires (par exemple, la modification d'un hachage modifie les deux). Le but principal du clonage d'un hachage est d'empêcher la modification de l'original (pour l'immuabilité, etc.).
K. Carpenter
6
@ K.Carpenter N'est-ce pas une copie superficielle qui partage des parties de l'original? La copie profonde, si je comprends bien, est une copie qui ne partage aucune partie de l'original, donc la modification de l'un ne modifiera pas l'autre.
Wayne Conrad
1
Quelle est exactement Marshal.load(Marshal.dump(o))la copie en profondeur? Je ne peux pas vraiment comprendre ce qui se passe dans les coulisses
Muntasir Alam
Ce que cela met également en évidence, c'est que si vous h1[:a] << 'bar'modifiez l'objet d'origine (la chaîne pointée par h1 [: a]) mais si vous deviez le faire à la h1[:a] = "#{h1[:a]}bar"place, vous créeriez un nouvel objet chaîne et le pointeriez h1[:a]là, tandis que h2[:a]est pointant toujours vers l'ancienne chaîne (non modifiée).
Max Williams
@MuntasirAlam J'ai ajouté quelques mots sur ce que fait le marshalling. J'espère que ça aide.
Wayne Conrad
73

Si vous utilisez Rails, vous pouvez faire:

h1 = h0.deep_dup

http://apidock.com/rails/Hash/deep_dup

lmanners
la source
2
Rails 3 a un problème avec les tableaux deep_duping dans Hashes. Rails 4 corrige cela.
pdobb
1
Merci de l'avoir signalé, mon hachage a toujours été affecté lors de l'utilisation de dup ou de clone
Esgi Dendyanri
13

Hash peut créer un nouveau hachage à partir d'un hachage existant:

irb(main):009:0> h1 = {1 => 2}
=> {1=>2}
irb(main):010:0> h2 = Hash[h1]
=> {1=>2}
irb(main):011:0> h1.object_id
=> 2150233660
irb(main):012:0> h2.object_id
=> 2150205060
James Moore
la source
24
Notez que cela a le même problème de copie profonde que #clone et #dup.
forforf
3
@forforf est correct. N'essayez pas de copier des structures de données si vous ne comprenez pas la copie profonde vs superficielle.
James Moore
5

Je suis également un débutant à Ruby et j'ai rencontré des problèmes similaires lors de la duplication d'un hachage. Utilisez le suivant. Je n'ai aucune idée de la vitesse de cette méthode.

copy_of_original_hash = Hash.new.merge(original_hash)
Kapil Aggarwal
la source
3

Comme mentionné dans la section Considérations sur la sécurité de la documentation de Marshal ,

Si vous devez désérialiser des données non fiables, utilisez JSON ou un autre format de sérialisation qui ne peut charger que des types simples et «primitifs» tels que String, Array, Hash, etc.

Voici un exemple sur la façon de cloner à l'aide de JSON dans Ruby:

require "json"

original = {"John"=>"Adams","Thomas"=>"Jefferson","Johny"=>"Appleseed"}
cloned = JSON.parse(JSON.generate(original))

# Modify original hash
original["John"] << ' Sandler'
p original 
#=> {"John"=>"Adams Sandler", "Thomas"=>"Jefferson", "Johny"=>"Appleseed"}

# cloned remains intact as it was deep copied
p cloned  
#=> {"John"=>"Adams", "Thomas"=>"Jefferson", "Johny"=>"Appleseed"}
Fabricant de baguette
la source
1

Utilisation Object#clone:

h1 = h0.clone

(Confusément, la documentation de cloneindique que initialize_copyc'est le moyen de contourner cela, mais le lien pour cette méthode Hashvous indique à la replaceplace ...)

Josh Lee
la source
1

Étant donné que la méthode de clonage standard préserve l'état figé, elle ne convient pas pour créer de nouveaux objets immuables basés sur l'objet d'origine, si vous souhaitez que les nouveaux objets soient légèrement différents de l'original (si vous aimez la programmation sans état).

kuonirat
la source
1

Le clone est lent. Car les performances devraient probablement commencer par un hachage vierge et une fusion. Ne couvre pas le cas des hachages imbriqués ...

require 'benchmark'

def bench  Benchmark.bm do |b|    
    test = {'a' => 1, 'b' => 2, 'c' => 3, 4 => 'd'}
    b.report 'clone' do
      1_000_000.times do |i|
        h = test.clone
        h['new'] = 5
      end
    end
    b.report 'merge' do
      1_000_000.times do |i|
        h = {}
        h['new'] = 5
        h.merge! test
      end
    end
    b.report 'inject' do
      1_000_000.times do |i|
        h = test.inject({}) do |n, (k, v)|
          n[k] = v;
          n
        end
        h['new'] = 5
      end
    end
  end
end
  système utilisateur de banc total (réel)
  clone 1,960000 0,080000 2,040000 (2,029604)
  fusionner 1,690000 0,080000 1,770000 (1,767828)
  injecter 3.120000 0,030000 3,150000 (3,152627)
  
Justin
la source
1

C'est un cas particulier, mais si vous commencez avec un hachage prédéfini que vous souhaitez récupérer et faire une copie, vous pouvez créer une méthode qui renvoie un hachage:

def johns 
    {  "John"=>"Adams","Thomas"=>"Jefferson","Johny"=>"Appleseed"}
end

h1 = johns

Le scénario particulier que j'avais était que j'avais une collection de hachages de schéma JSON où certains hachages se sont construits sur d'autres. Je les définissais initialement comme des variables de classe et j'ai rencontré ce problème de copie.

grumpasaurus
la source
0

vous pouvez utiliser ci-dessous pour copier en profondeur les objets Hash.

deeply_copied_hash = Marshal.load(Marshal.dump(original_hash))
ktsujister
la source
16
Ceci est un double de la réponse de Wayne Conrad.
Andrew Grimm
0

Puisque Ruby a un million de façons de le faire, voici une autre façon d'utiliser Enumerable:

h0 = {  "John"=>"Adams","Thomas"=>"Jefferson","Johny"=>"Appleseed"}
h1 = h0.inject({}) do |new, (name, value)| 
    new[name] = value;
    new 
end
Rohit
la source
-3

Une manière alternative à Deep_Copy qui a fonctionné pour moi.

h1 = {:a => 'foo'} 
h2 = Hash[h1.to_a]

Cela a produit une copie profonde puisque h2 est formé en utilisant une représentation matricielle de h1 plutôt que des références de h1.

user2521734
la source
3
Cela semble prometteur mais ne fonctionne pas, ceci est une autre copie peu profonde
Ginty