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_set
diese umgehen dies, müssen jedoch für die Metaprogrammierung vorhanden sein, da der l-Wert in =
nicht dynamisch sein kann.