Seltsames, unerwartetes Verhalten (Verschwinden / Ändern von Werten) bei Verwendung des Hash-Standardwerts, z. B. Hash.new ([])

107

Betrachten Sie diesen Code:

h = Hash.new(0)  # New hash pairs will by default have 0 as values
h[1] += 1  #=> {1=>1}
h[2] += 2  #=> {2=>2}

Das ist alles in Ordnung, aber:

h = Hash.new([])  # Empty array as default value
h[1] <<= 1  #=> {1=>[1]}                  ← Ok
h[2] <<= 2  #=> {1=>[1,2], 2=>[1,2]}      ← Why did `1` change?
h[3] << 3   #=> {1=>[1,2,3], 2=>[1,2,3]}  ← Where is `3`?

An diesem Punkt erwarte ich den Hash:

{1=>[1], 2=>[2], 3=>[3]}

aber davon ist es weit entfernt. Was passiert und wie kann ich das erwartete Verhalten erreichen?

Valentin Vasilyev
quelle

Antworten:

164

Beachten Sie zunächst, dass dieses Verhalten für alle Standardwerte gilt, die anschließend mutiert werden (z. B. Hashes und Zeichenfolgen), nicht nur für Arrays.

TL; DR : Verwenden Hash.new { |h, k| h[k] = [] }Sie diese Option, wenn Sie die idiomatischste Lösung suchen und sich nicht darum kümmern, warum.


Was nicht funktioniert

Warum Hash.new([])funktioniert das nicht?

Schauen wir uns genauer an, warum Hash.new([])das nicht funktioniert:

h = Hash.new([])
h[0] << 'a'  #=> ["a"]
h[1] << 'b'  #=> ["a", "b"]
h[1]         #=> ["a", "b"]

h[0].object_id == h[1].object_id  #=> true
h  #=> {}

Wir können sehen, dass unser Standardobjekt wiederverwendet und mutiert wird (dies liegt daran, dass es als einziger Standardwert übergeben wird und der Hash keine Möglichkeit hat, einen neuen Standardwert zu erhalten), aber warum gibt es keine Schlüssel oder Werte im Array, obwohl h[1]wir immer noch einen Wert haben? Hier ist ein Hinweis:

h[42]  #=> ["a", "b"]

Das von jedem []Aufruf zurückgegebene Array ist nur der Standardwert, den wir die ganze Zeit mutiert haben und der jetzt unsere neuen Werte enthält. Da <<es keine Zuordnung zum Hash gibt (es kann in Ruby niemals eine Zuordnung ohne =Geschenk geben ), haben wir nie etwas in unseren eigentlichen Hash eingefügt. Stattdessen müssen wir verwenden <<=(was zu ist <<wie +=zu +):

h[2] <<= 'c'  #=> ["a", "b", "c"]
h             #=> {2=>["a", "b", "c"]}

Dies ist das gleiche wie:

h[2] = (h[2] << 'c')

Warum Hash.new { [] }funktioniert das nicht?

Die Verwendung Hash.new { [] }löst das Problem der Wiederverwendung und Mutation des ursprünglichen Standardwerts (da der angegebene Block jedes Mal aufgerufen wird und ein neues Array zurückgibt), nicht jedoch das Zuweisungsproblem:

h = Hash.new { [] }
h[0] << 'a'   #=> ["a"]
h[1] <<= 'b'  #=> ["b"]
h             #=> {1=>["b"]}

Was funktioniert?

Der Zuweisungsweg

Wenn wir daran denken, immer zu verwenden <<=, dann Hash.new { [] } ist dies eine praktikable Lösung, aber es ist ein bisschen seltsam und nicht idiomatisch (ich habe es noch nie <<=in freier Wildbahn gesehen). Es ist auch anfällig für subtile Fehler, wenn <<es versehentlich verwendet wird.

Der veränderliche Weg

Die Dokumentation fürHash.new Staaten (Schwerpunkt meine eigene):

Wenn ein Block angegeben wird, wird er mit dem Hash-Objekt und dem Schlüssel aufgerufen und sollte den Standardwert zurückgeben. Es liegt in der Verantwortung des Blocks, den Wert bei Bedarf im Hash zu speichern .

Wir müssen also den Standardwert im Hash innerhalb des Blocks speichern, wenn wir <<anstelle von <<=:

h = Hash.new { |h, k| h[k] = [] }
h[0] << 'a'  #=> ["a"]
h[1] << 'b'  #=> ["b"]
h            #=> {0=>["a"], 1=>["b"]}

Dadurch wird die Zuweisung effektiv von unseren einzelnen Aufrufen (die verwendet werden würden <<=) zu dem Block verschoben, an den übergeben wird Hash.new, wodurch die Last unerwarteten Verhaltens bei der Verwendung beseitigt wird <<.

Beachten Sie, dass es einen funktionalen Unterschied zwischen dieser Methode und den anderen gibt: Auf diese Weise wird der Standardwert beim Lesen zugewiesen (da die Zuweisung immer innerhalb des Blocks erfolgt). Beispielsweise:

h1 = Hash.new { |h, k| h[k] = [] }
h1[:x]
h1  #=> {:x=>[]}

h2 = Hash.new { [] }
h2[:x]
h2  #=> {}

Der unveränderliche Weg

Sie fragen sich vielleicht, warum Hash.new([])es nicht funktioniert, solange es gut Hash.new(0)funktioniert. Der Schlüssel ist, dass Numerics in Ruby unveränderlich sind, sodass wir sie natürlich nie an Ort und Stelle mutieren. Wenn wir unseren Standardwert als unveränderlich behandeln würden, könnten wir auch gut verwenden Hash.new([]):

h = Hash.new([].freeze)
h[0] += ['a']  #=> ["a"]
h[1] += ['b']  #=> ["b"]
h[2]           #=> []
h              #=> {0=>["a"], 1=>["b"]}

Beachten Sie jedoch, dass ([].freeze + [].freeze).frozen? == false. Wenn Sie also sicherstellen möchten, dass die Unveränderlichkeit durchgehend erhalten bleibt, müssen Sie darauf achten, das neue Objekt erneut einzufrieren.


Fazit

Von allen Möglichkeiten bevorzuge ich persönlich den „unveränderlichen Weg“ - Unveränderlichkeit macht das Denken über Dinge im Allgemeinen viel einfacher. Es ist schließlich die einzige Methode, bei der kein verstecktes oder subtiles unerwartetes Verhalten möglich ist. Der gebräuchlichste und idiomatischste Weg ist jedoch der „veränderbare Weg“.

Abgesehen davon wird dieses Verhalten der Hash-Standardwerte in Ruby Koans vermerkt .


Dies ist nicht unbedingt der Fall, Methoden wie instance_variable_setdiese umgehen dies, müssen jedoch für die Metaprogrammierung vorhanden sein, da der l-Wert in =nicht dynamisch sein kann.

Andrew Marshall
quelle
1
Es ist zu erwähnen, dass die Verwendung des "veränderlichen Weges" auch dazu führt, dass bei jeder Hash-Suche ein Schlüsselwertpaar gespeichert wird (da im Block eine Zuweisung stattfindet), was möglicherweise nicht immer erwünscht ist.
Johncip
@johncip Nicht jede Suche, nur die erste für jeden Schlüssel. Aber ich verstehe, was du meinst, das werde ich später zur Antwort hinzufügen. Vielen Dank!.
Andrew Marshall
Hoppla, schlampig zu sein. Sie haben natürlich Recht, es ist die erste Suche nach einem unbekannten Schlüssel. Ich fühle mich fast wie { [] }mit <<=hat die wenigstenen Überraschungen, war es nicht für die Tatsache , dass die versehentlich vergessen =zu einer sehr verwirrend Debug - Sitzung führen könnte.
Johncip
ziemlich klare Erklärungen über Unterschiede beim Initialisieren von Hash mit Standardwerten
cisolarix
23

Sie geben an, dass der Standardwert für den Hash ein Verweis auf dieses bestimmte (anfangs leere) Array ist.

Ich denke du willst:

h = Hash.new { |hash, key| hash[key] = []; }
h[1]<<=1 
h[2]<<=2 

Dadurch wird der Standardwert für jeden Schlüssel auf ein neues Array festgelegt.

Matthew Flaschen
quelle
Wie kann ich für jeden neuen Hash separate Array-Instanzen verwenden?
Valentin Vasilyev
5
Diese Blockversion gibt Ihnen Arraybei jedem Aufruf neue Instanzen. Das heißt : h = Hash.new { |hash, key| hash[key] = []; puts hash[key].object_id }; h[1] # => 16348490; h[2] # => 16346570. Außerdem: Wenn Sie die Blockversion verwenden, die den Wert ( {|hash,key| hash[key] = []}) festlegt, und nicht die, die einfach den Wert ( ) generiert{ [] } , benötigen Sie nur <<, nicht <<=beim Hinzufügen von Elementen.
James A. Rosen
3

Der Operator arbeitet, +=wenn er auf diese Hashes angewendet wird, wie erwartet.

[1] pry(main)> foo = Hash.new( [] )
=> {}
[2] pry(main)> foo[1]+=[1]
=> [1]
[3] pry(main)> foo[2]+=[2]
=> [2]
[4] pry(main)> foo
=> {1=>[1], 2=>[2]}
[5] pry(main)> bar = Hash.new { [] }
=> {}
[6] pry(main)> bar[1]+=[1]
=> [1]
[7] pry(main)> bar[2]+=[2]
=> [2]
[8] pry(main)> bar
=> {1=>[1], 2=>[2]}

Dies kann daran foo[bar]+=bazist syntaktischer Zucker für , foo[bar]=foo[bar]+bazwenn foo[bar]auf der rechten Hand =ausgewertet das zurückStandardWert Objekts und die +Bediener nicht ändern. Die linke Hand ist syntaktischer Zucker für die []=Methode, die den Standardwert nicht ändert .

Beachten Sie, dass dies nicht gilt , foo[bar]<<=bazwie es gleichwertig sein wird foo[bar]=foo[bar]<<bazund << wird den ändern Standardwert .

Auch fand ich keinen Unterschied zwischen Hash.new{[]}und Hash.new{|hash, key| hash[key]=[];}. Zumindest auf Rubin 2.1.2.

Daniel Ribeiro Moreira
quelle
Schöne Erklärung. Es scheint, als ob auf Ruby 2.1.1 Hash.new{[]}dasselbe ist wie Hash.new([])für mich mit dem Mangel an erwartetem <<Verhalten (obwohl es natürlich Hash.new{|hash, key| hash[key]=[];}funktioniert). Seltsame kleine Dinge, die alle Dinge zerbrechen: /
Butterywombat
1

Wenn Sie schreiben,

h = Hash.new([])

Sie übergeben die Standardreferenz des Arrays an alle Elemente im Hash. Aus diesem Grund verweisen alle Elemente im Hash auf dasselbe Array.

Wenn Sie möchten, dass jedes Element im Hash auf ein separates Array verweist, sollten Sie verwenden

h = Hash.new{[]} 

Weitere Informationen zur Funktionsweise von Ruby finden Sie hier: http://ruby-doc.org/core-2.2.0/Array.html#method-c-new

Ganesh Sagare
quelle
Das ist falsch, Hash.new { [] }funktioniert nicht . Siehe meine Antwort für Details. Es ist auch schon die in einer anderen Antwort vorgeschlagene Lösung.
Andrew Marshall