Comportamento strano e inaspettato (scomparsa / modifica dei valori) quando si utilizza il valore predefinito Hash, ad esempio Hash.new ([])


107

Considera questo codice:

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

Va tutto bene, ma:

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`?

A questo punto mi aspetto che l'hash sia:

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

ma è lontano da quello. Cosa sta succedendo e come posso ottenere il comportamento che mi aspetto?

Risposte:


164

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_setbypassare questo, ma devono esistere per la metaprogrammazione poiché il valore l in =non può essere dinamico.


1
Vale la pena ricordare che l'uso del "modo mutabile" ha anche l'effetto di far sì che ogni ricerca hash memorizzi una coppia di valori chiave (poiché nel blocco si verifica un'assegnazione), il che potrebbe non essere sempre desiderato.
johncip

@johncip Non tutte le ricerche , solo la prima per ogni chiave. Ma capisco cosa intendi, lo aggiungerò alla risposta più tardi; Grazie!.
Andrew Marshall

Ops, essendo sciatto. Hai ragione, ovviamente, è la prima ricerca di una chiave sconosciuta. Mi sembra quasi che { [] }con <<=abbia il minor numero di sorprese, se non fosse per il fatto che dimenticarlo accidentalmente =potrebbe portare a una sessione di debug molto confusa.
johncip

spiegazioni abbastanza chiare sulle differenze quando si inizializza l'hash con i valori predefiniti
cisolarix

23

Stai specificando che il valore predefinito per l'hash è un riferimento a quel particolare array (inizialmente vuoto).

Penso che tu voglia:

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

Ciò imposta il valore predefinito per ogni chiave su un nuovo array.


Come posso utilizzare istanze di array separate per ogni nuovo hash?
Valentin Vasilyev,

5
Quella versione in blocco ti offre nuove Arrayistanze a ogni chiamata. Vale a dire: h = Hash.new { |hash, key| hash[key] = []; puts hash[key].object_id }; h[1] # => 16348490; h[2] # => 16346570. Inoltre: se usi la versione a blocchi che imposta value ( {|hash,key| hash[key] = []}) invece di quella che genera semplicemente value ( { [] }), allora hai solo bisogno <<, non <<=quando aggiungi elementi.
James A. Rosen

3

L'operatore +=quando applicato a quegli hash funziona come previsto.

[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]}

Ciò può essere dovuto al fatto che foo[bar]+=bazè zucchero sintattico perché foo[bar]=foo[bar]+bazquando foo[bar]sulla destra di =viene valutato restituisce l' oggetto valore predefinito e l' +operatore non lo cambierà. La mano sinistra è lo zucchero sintattico per il []=metodo che non cambierà il valore predefinito .

Si noti che questo non si applica a foo[bar]<<=bazcome sarà equivalente a foo[bar]=foo[bar]<<baze << sarà modificare il valore predefinito .

Inoltre, non ho trovato differenze tra Hash.new{[]}e Hash.new{|hash, key| hash[key]=[];}. Almeno su rubino 2.1.2.


Bella spiegazione. Sembra che su ruby ​​2.1.1 Hash.new{[]}sia lo stesso Hash.new([])di me con la mancanza di <<comportamento previsto (anche se ovviamente Hash.new{|hash, key| hash[key]=[];}funziona). Strane piccole cose che rompono tutte le cose: /
butterywombat

1

Quando scrivi,

h = Hash.new([])

si passa il riferimento predefinito di array a tutti gli elementi in hash. per questo motivo tutti gli elementi in hash fanno riferimento allo stesso array.

se vuoi che ogni elemento in hash faccia riferimento a un array separato, dovresti usare

h = Hash.new{[]} 

per maggiori dettagli su come funziona in ruby, segui questo: http://ruby-doc.org/core-2.2.0/Array.html#method-c-new


Questo è sbagliato, Hash.new { [] }non non funziona. Vedi la mia risposta per i dettagli. È anche già la soluzione proposta in un'altra risposta.
Andrew Marshall
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.