Wie kopiere ich einen Hash in Ruby?

197

Ich gebe zu, dass ich ein bisschen ein Rubin-Neuling bin (jetzt schreibe ich Rake-Skripte). In den meisten Sprachen sind Kopierkonstruktoren leicht zu finden. Eine halbe Stunde Suche fand es nicht in Rubin. Ich möchte eine Kopie des Hashs erstellen, damit ich ihn ändern kann, ohne die ursprüngliche Instanz zu beeinflussen.

Einige erwartete Methoden, die nicht wie beabsichtigt funktionieren:

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

In der Zwischenzeit habe ich auf diese unelegante Problemumgehung zurückgegriffen

def copyhash(inputhash)
  h = Hash.new
  inputhash.each do |pair|
    h.store(pair[0], pair[1])
  end
  return h
end
Niederschlagend
quelle
Wenn Sie mit einfachen HashObjekten arbeiten, ist die Antwort gut. Wenn Sie mit Hash-ähnlichen Objekten arbeiten, die von Orten stammen, die Sie nicht kontrollieren, sollten Sie überlegen, ob die dem Hash zugeordnete Singleton-Klasse dupliziert werden soll oder nicht. Siehe stackoverflow.com/questions/10183370/…
Sim

Antworten:

223

Die cloneMethode ist Rubys Standardmethode für die Erstellung einer flachen Kopie :

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"}

Beachten Sie, dass das Verhalten möglicherweise überschrieben wird:

Diese Methode kann klassenspezifisches Verhalten aufweisen. In diesem Fall wird dieses Verhalten nach der #initialize_copyMethode der Klasse dokumentiert .

Mark Rushakoff
quelle
Klonen ist eine Methode für Object, BTW, also hat alles Zugriff darauf. Siehe die API-Details hier
Dylan Lacey
29
Wenn Sie hier einen expliziteren Kommentar für diejenigen hinzufügen, die keine anderen Antworten lesen, handelt es sich um eine flache Kopie.
Grumpasaurus
Die Dokumentation #initialize_copy scheint für Hash nicht zu existieren, obwohl auf der Hash-Dokumentseite ruby-doc.org/core-1.9.3/Hash.html#method-i-initialize_copy
philwhln
14
Und für andere Ruby-Anfänger bedeutet "flache Kopie", dass jedes Objekt unter der ersten Ebene immer noch eine Referenz ist.
RobW
9
Beachten Sie, dass dies für verschachtelte Hashes bei mir nicht funktioniert hat (wie in anderen Antworten erwähnt). Ich habe benutzt Marshal.load(Marshal.dump(h)).
Bheeshmar
178

Wie andere darauf hingewiesen haben, clonewird es tun. Beachten Sie, dass cloneein Hash eine flache Kopie erstellt. Das heißt:

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

Was passiert ist, dass die Referenzen des Hashs kopiert werden, aber nicht die Objekte, auf die sich die Referenzen beziehen.

Wenn Sie eine tiefe Kopie wünschen, dann:

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_copyfunktioniert für jedes Objekt, das gemarshallt werden kann. Die meisten integrierten Datentypen (Array, Hash, String usw.) können gemarshallt werden.

Marshalling ist Rubys Name für Serialisierung . Beim Marshalling wird das Objekt - mit den Objekten, auf die es sich bezieht - in eine Reihe von Bytes konvertiert. Diese Bytes werden dann verwendet, um ein anderes Objekt wie das Original zu erstellen.

Wayne Conrad
quelle
Es ist schön, dass Sie die Informationen zum Tiefenkopieren angegeben haben, aber es sollte eine Warnung enthalten sein, dass dies unbeabsichtigte Nebenwirkungen verursachen kann (wenn Sie beispielsweise einen der beiden Hashs ändern, werden beide geändert). Der Hauptzweck des Klonens eines Hashs besteht darin, eine Änderung des Originals zu verhindern (aus Gründen der Unveränderlichkeit usw.).
K. Carpenter
6
@ K.Carpenter Ist es nicht eine flache Kopie, die Teile des Originals teilt? Deep Copy ist, wie ich es verstehe, eine Kopie, die keinen Teil des Originals teilt. Wenn Sie also eine ändern, wird die andere nicht geändert.
Wayne Conrad
1
Wie genau wird Marshal.load(Marshal.dump(o))tief kopiert? Ich kann nicht wirklich verstehen, was hinter den Kulissen passiert
Muntasir Alam
Dies unterstreicht auch, dass Sie, wenn Sie h1[:a] << 'bar'das ursprüngliche Objekt ändern (die Zeichenfolge, auf die h1 [: a] zeigt), aber wenn Sie dies h1[:a] = "#{h1[:a]}bar"stattdessen tun würden, ein neues Zeichenfolgenobjekt erstellen und darauf zeigen würden h1[:a], während es h2[:a]ist zeigt immer noch auf die alte (unveränderte) Zeichenfolge.
Max Williams
@MuntasirAlam Ich habe ein paar Worte darüber hinzugefügt, was Marshalling macht. Ich hoffe das hilft.
Wayne Conrad
73

Wenn Sie Rails verwenden, können Sie Folgendes tun:

h1 = h0.deep_dup

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

lmanners
quelle
2
Rails 3 hat ein Problem mit deep_duping-Arrays in Hashes. Rails 4 behebt dieses Problem.
pdobb
1
Vielen Dank für den Hinweis, dass mein Hash immer noch betroffen ist, wenn ich Dup oder Klon
benutze
13

Hash kann aus einem vorhandenen Hash einen neuen Hash erstellen:

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
quelle
24
Beachten Sie, dass dies das gleiche Problem mit tiefen Kopien wie #clone und #dup hat.
Forforf
3
@forforf ist korrekt. Versuchen Sie nicht, Datenstrukturen zu kopieren, wenn Sie keine tiefe oder flache Kopie verstehen.
James Moore
5

Ich bin auch ein Neuling bei Ruby und hatte ähnliche Probleme beim Duplizieren eines Hashs. Verwenden Sie Folgendes. Ich habe keine Ahnung von der Geschwindigkeit dieser Methode.

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

Wie im Abschnitt "Sicherheitsüberlegungen" der Marshal-Dokumentation erwähnt ,

Wenn Sie nicht vertrauenswürdige Daten deserialisieren müssen, verwenden Sie JSON oder ein anderes Serialisierungsformat, mit dem nur einfache, primitive Typen wie String, Array, Hash usw. geladen werden können.

Hier ist ein Beispiel zum Klonen mit JSON in 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"}
Zauberstabmacher
quelle
1

Verwendung Object#clone:

h1 = h0.clone

(Verwirrenderweise heißt es in der Dokumentation zu clone, dass dies initialize_copyder Weg ist, dies zu überschreiben, aber der Link für diese Methode in Hashverweist Sie replacestattdessen auf ...)

Josh Lee
quelle
1

Da die Standardklonmethode den eingefrorenen Zustand beibehält, eignet sie sich nicht zum Erstellen neuer unveränderlicher Objekte auf der Grundlage des Originalobjekts, wenn Sie möchten, dass sich die neuen Objekte geringfügig vom Original unterscheiden (wenn Sie zustandslose Programmierung wünschen).

kuonirat
quelle
1

Klon ist langsam. Für die Leistung sollte wahrscheinlich mit leerem Hash beginnen und zusammenführen. Deckt nicht den Fall verschachtelter Hashes ab ...

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
  Bench User System insgesamt (real)
  Klon 1.960000 0.080000 2.040000 (2.029604)
  Zusammenführen 1.690000 0.080000 1.770000 (1.767828)
  injizieren 3.120000 0.030000 3.150000 (3.152627)
  
Justin
quelle
1

Dies ist ein Sonderfall. Wenn Sie jedoch mit einem vordefinierten Hash beginnen, von dem Sie eine Kopie erstellen möchten, können Sie eine Methode erstellen, die einen Hash zurückgibt:

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

h1 = johns

Das besondere Szenario, das ich hatte, war, dass ich eine Sammlung von JSON-Schema-Hashes hatte, in denen einige Hashes aus anderen aufgebaut waren. Ich habe sie ursprünglich als Klassenvariablen definiert und bin auf dieses Kopierproblem gestoßen.

Grumpasaurus
quelle
0

Sie können unten verwenden, um Hash-Objekte tief zu kopieren.

deeply_copied_hash = Marshal.load(Marshal.dump(original_hash))
ktsujister
quelle
16
Dies ist ein Duplikat von Wayne Conrads Antwort.
Andrew Grimm
0

Da Ruby über eine Million Möglichkeiten verfügt, können Sie Enumerable auf folgende Weise verwenden:

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

Alternativer Weg zu Deep_Copy, der für mich funktioniert hat.

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

Dies erzeugte eine deep_copy, da h2 unter Verwendung einer Array-Darstellung von h1 anstelle der Referenzen von h1 gebildet wird.

user2521734
quelle
3
Klingt vielversprechend, funktioniert aber nicht, dies ist eine weitere flache Kopie
Ginty