Innanzitutto, nota che questo comportamento si applica a qualsiasi valore predefinito che viene successivamente modificato (ad es. Hash e stringhe), non solo agli array.
TL; DR : Usa Hash.new { |h, k| h[k] = [] }
se vuoi la soluzione più idiomatica e non ti interessa perché.
Cosa non funziona
Perché Hash.new([])
non funziona
Diamo un'occhiata più approfondita al motivo per cui Hash.new([])
non funziona:
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 #=> {}
Possiamo vedere che il nostro oggetto predefinito viene riutilizzato e modificato (questo perché viene passato come unico valore predefinito, l'hash non ha modo di ottenere un nuovo valore predefinito), ma perché non ci sono chiavi o valori nell'array, nonostante h[1]
ci dia ancora un valore? Ecco un suggerimento:
h[42] #=> ["a", "b"]
L'array restituito da ogni []
chiamata è solo il valore predefinito, che abbiamo modificato per tutto questo tempo, quindi ora contiene i nostri nuovi valori. Dato <<
che non assegna all'hash (non può mai esserci assegnazione in Ruby senza un =
regalo † ), non abbiamo mai inserito nulla nel nostro hash effettivo. Invece dobbiamo usare <<=
(che sta a <<
come +=
sta a +
):
h[2] <<= 'c' #=> ["a", "b", "c"]
h #=> {2=>["a", "b", "c"]}
Questo è lo stesso di:
h[2] = (h[2] << 'c')
Perché Hash.new { [] }
non funziona
L'utilizzo Hash.new { [] }
risolve il problema del riutilizzo e della modifica del valore predefinito originale (poiché il blocco fornito viene chiamato ogni volta, restituendo un nuovo array), ma non il problema dell'assegnazione:
h = Hash.new { [] }
h[0] << 'a' #=> ["a"]
h[1] <<= 'b' #=> ["b"]
h #=> {1=>["b"]}
Cosa funziona
Il modo di assegnazione
Se ci ricordiamo di usarlo sempre <<=
, allora Hash.new { [] }
è una soluzione praticabile, ma è un po 'strano e non idiomatico (non l'ho mai visto <<=
usato in natura). È anche soggetto a piccoli bug se <<
viene utilizzato inavvertitamente.
Il modo mutevole
La documentazione per gliHash.new
stati (enfasi mia):
Se viene specificato un blocco, verrà chiamato con l'oggetto hash e la chiave e dovrebbe restituire il valore predefinito. È responsabilità del blocco memorizzare il valore nell'hash, se necessario .
Quindi dobbiamo memorizzare il valore predefinito nell'hash dall'interno del blocco se vogliamo usare <<
invece di <<=
:
h = Hash.new { |h, k| h[k] = [] }
h[0] << 'a' #=> ["a"]
h[1] << 'b' #=> ["b"]
h #=> {0=>["a"], 1=>["b"]}
Questo sposta efficacemente l'assegnazione dalle nostre chiamate individuali (che useremmo <<=
) al blocco passato Hash.new
, rimuovendo l'onere di comportamenti imprevisti durante l'utilizzo <<
.
Notare che c'è una differenza funzionale tra questo metodo e gli altri: in questo modo si assegna il valore di default in fase di lettura (poiché l'assegnazione avviene sempre all'interno del blocco). Per esempio:
h1 = Hash.new { |h, k| h[k] = [] }
h1[:x]
h1 #=> {:x=>[]}
h2 = Hash.new { [] }
h2[:x]
h2 #=> {}
Il modo immutabile
Forse ti starai chiedendo perché Hash.new([])
non funziona mentre Hash.new(0)
funziona bene. La chiave è che i numeri in Ruby sono immutabili, quindi naturalmente non finiamo mai per modificarli sul posto. Se trattassimo il nostro valore predefinito come immutabile, potremmo usare Hash.new([])
anche bene:
h = Hash.new([].freeze)
h[0] += ['a'] #=> ["a"]
h[1] += ['b'] #=> ["b"]
h[2] #=> []
h #=> {0=>["a"], 1=>["b"]}
Tuttavia, tieni presente che ([].freeze + [].freeze).frozen? == false
. Quindi, se vuoi assicurarti che l'immutabilità sia preservata per tutto il tempo, devi fare attenzione a ricongelare il nuovo oggetto.
Conclusione
Di tutti i modi, personalmente preferisco "la via immutabile": l'immutabilità generalmente rende il ragionamento sulle cose molto più semplice. Dopotutto, è l'unico metodo che non ha possibilità di comportamenti nascosti o subdoli inaspettati. Tuttavia, il modo più comune e idiomatico è "il modo mutevole".
Infine , questo comportamento dei valori predefiniti di Hash è notato in Ruby Koans .
† Questo non è strettamente vero, metodi come instance_variable_set
bypassare questo, ma devono esistere per la metaprogrammazione poiché il valore l in =
non può essere dinamico.