Dziwne, nieoczekiwane zachowanie (znikanie / zmiana wartości) podczas korzystania z domyślnej wartości Hash, np. Hash.new ([])

107

Rozważ ten kod:

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

Wszystko w porządku, ale:

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

W tym momencie spodziewam się, że hash będzie:

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

ale to dalekie od tego. Co się dzieje i jak mogę uzyskać oczekiwane zachowanie?

Walentyna Wasiljewa
źródło

Odpowiedzi:

164

Po pierwsze, zwróć uwagę, że to zachowanie dotyczy każdej wartości domyślnej, która jest następnie modyfikowana (np. Skróty i ciągi znaków), a nie tylko tablice.

TL; DR : użyj, Hash.new { |h, k| h[k] = [] }jeśli chcesz najbardziej idiomatycznego rozwiązania i nie obchodzi cię dlaczego.


Co nie działa

Dlaczego Hash.new([])nie działa

Przyjrzyjmy się dokładniej, dlaczego Hash.new([])nie działa:

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  #=> {}

Widzimy, że nasz domyślny obiekt jest ponownie używany i mutowany (to dlatego, że jest przekazywany jako jedyna wartość domyślna, hash nie ma możliwości uzyskania nowej, nowej wartości domyślnej), ale dlaczego nie ma kluczy ani wartości w tablicy, mimo że h[1]nadal podaje nam wartość? Oto wskazówka:

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

Tablica zwracana przez każde []wywołanie jest po prostu wartością domyślną, którą przez cały ten czas modyfikowaliśmy, więc teraz zawiera nasze nowe wartości. Ponieważ <<nie przypisuje się do skrótu (w Rubim nigdy nie ma przypisania bez =prezentu ), nigdy nie wstawialiśmy niczego do naszego aktualnego skrótu. Zamiast tego musimy użyć <<=(co jest <<tak, jak +=jest +):

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

To jest to samo, co:

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

Dlaczego Hash.new { [] }nie działa

Użycie Hash.new { [] }rozwiązuje problem ponownego użycia i mutowania pierwotnej wartości domyślnej (ponieważ dany blok jest wywoływany za każdym razem, zwracając nową tablicę), ale nie problem z przypisaniem:

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

Co działa

Sposób przydziału

Jeśli pamiętamy, aby zawsze używać <<=, Hash.new { [] } jest to realne rozwiązanie, ale jest trochę dziwne i nie idiomatyczne (nigdy nie widziałem <<=używanego na wolności). Jest również podatny na subtelne błędy, jeśli <<jest nieumyślnie używany.

Zmienny sposób

Dokumentacja dlaHash.new stanów (kursywa moja własna):

Jeśli określono blok, zostanie on wywołany z obiektem skrótu i ​​kluczem i powinien zwrócić wartość domyślną. W razie potrzeby blok jest odpowiedzialny za przechowywanie wartości w skrócie .

Musimy więc przechowywać domyślną wartość w hashu z wewnątrz bloku, jeśli <<zamiast tego chcemy użyć <<=:

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

To skutecznie przenosi przypisanie z naszych indywidualnych wywołań (których użyje <<=) do bloku przekazanego do Hash.new, usuwając ciężar nieoczekiwanego zachowania podczas używania <<.

Zwróć uwagę, że istnieje jedna funkcjonalna różnica między tą metodą a innymi: w ten sposób przypisuje się wartość domyślną podczas odczytu (ponieważ przypisanie zawsze ma miejsce wewnątrz bloku). Na przykład:

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

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

Niezmienny sposób

Możesz się zastanawiać, dlaczego Hash.new([])nie działa, a Hash.new(0)działa dobrze. Kluczem jest to, że liczby w Rubim są niezmienne, więc naturalnie nigdy nie zmutujemy ich w miejscu. Gdybyśmy traktowali naszą domyślną wartość jako niezmienną, moglibyśmy również użyć Hash.new([]):

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

Pamiętaj jednak, że ([].freeze + [].freeze).frozen? == false. Jeśli więc chcesz mieć pewność, że niezmienność zostanie zachowana przez cały czas, musisz zadbać o ponowne zamrożenie nowego obiektu.


Wniosek

Ze wszystkich sposobów osobiście wolę „niezmienny sposób” - niezmienność ogólnie sprawia, że ​​rozumowanie na różne tematy jest znacznie prostsze. W końcu jest to jedyna metoda, która nie ma możliwości ukrytego lub subtelnego nieoczekiwanego zachowania. Jednak najbardziej powszechnym i idiomatycznym sposobem jest „sposób zmienny”.

Na koniec, takie zachowanie wartości domyślnych Hash zostało odnotowane w Ruby Koans .


To nie jest do końca prawdą, metody takie jak instance_variable_setomijają to, ale muszą istnieć dla metaprogramowania, ponieważ wartość l w =nie może być dynamiczna.

Andrew Marshall
źródło
1
Warto wspomnieć, że użycie „mutowalnego sposobu” powoduje również, że każde wyszukiwanie skrótu przechowuje parę klucz-wartość (ponieważ w bloku ma miejsce przypisanie), co nie zawsze może być pożądane.
johncip
@johncip Nie każde wyszukiwanie, tylko pierwsze do każdego klucza. Ale rozumiem, co masz na myśli, dodam to później do odpowiedzi; dzięki!.
Andrew Marshall
Ups, niechlujstwo. Oczywiście masz rację, jest to pierwsze wyszukiwanie nieznanego klucza. Czuję się prawie jak { [] }z <<=ma najmniejszą liczbę niespodzianek, gdyby nie fakt, że przypadkowo zapominając =może prowadzić do bardzo mylące sesji debugowania.
johncip
dość jasne wyjaśnienia dotyczące różnic podczas inicjowania hasha z wartościami domyślnymi
cisolarix
23

Określasz, że domyślna wartość skrótu jest odniesieniem do tej konkretnej (początkowo pustej) tablicy.

Myślę, że chcesz:

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

To ustawia domyślną wartość każdego klucza na nową tablicę.

Matthew Flaschen
źródło
Jak mogę używać oddzielnych instancji tablic dla każdego nowego skrótu?
Valentin Wasiljew
5
Ta wersja blokowa zapewnia nowe Arrayinstancje przy każdym wywołaniu. Mianowicie: h = Hash.new { |hash, key| hash[key] = []; puts hash[key].object_id }; h[1] # => 16348490; h[2] # => 16346570. Ponadto: jeśli używasz wersji bloku, która ustawia wartość ( {|hash,key| hash[key] = []}), a nie tej, która po prostu generuje wartość ( { [] }), potrzebujesz tylko <<, a nie <<=podczas dodawania elementów.
James A. Rosen
3

Operator +=zastosowany do tych skrótów działa zgodnie z oczekiwaniami.

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

Może to być spowodowane tym, że foo[bar]+=bazjest to cukier składniowy, ponieważ foo[bar]=foo[bar]+bazgdy foo[bar]po prawej stronie =jest oceniany, zwraca domyślny obiekt wartości, a +operator jej nie zmieni. Lewa ręka to cukier składniowy dla []=metody, która nie zmieni wartości domyślnej .

Należy pamiętać, że to nie ma zastosowania w przypadku foo[bar]<<=bazgdy będzie to odpowiednik foo[bar]=foo[bar]<<bazi << będzie zmienić domyślną wartość .

Nie znalazłem również różnicy między Hash.new{[]}i Hash.new{|hash, key| hash[key]=[];}. Przynajmniej na ruby ​​2.1.2.

Daniel Ribeiro Moreira
źródło
Niezłe wyjaśnienie. Wygląda na Hash.new{[]}to, że Ruby 2.1.1 jest taki sam jak Hash.new([])dla mnie z brakiem oczekiwanego <<zachowania (choć oczywiście Hash.new{|hash, key| hash[key]=[];}działa). Dziwne małe rzeczy
psujące
1

Kiedy piszesz,

h = Hash.new([])

przekazujesz domyślne odniesienie tablicy do wszystkich elementów w hashu. z tego powodu wszystkie elementy w skrócie odnoszą się do tej samej tablicy.

jeśli chcesz, aby każdy element w skrócie odnosił się do oddzielnej tablicy, powinieneś użyć

h = Hash.new{[]} 

aby uzyskać więcej informacji o tym, jak to działa w Rubim, przejdź przez to: http://ruby-doc.org/core-2.2.0/Array.html#method-c-new

Ganesh Sagare
źródło
To nie tak, Hash.new { [] }nie nie działa. Zobacz moją odpowiedź po szczegóły. To też jest już rozwiązanie zaproponowane w innej odpowiedzi.
Andrew Marshall,