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_set
omijają to, ale muszą istnieć dla metaprogramowania, ponieważ wartość l w =
nie może być dynamiczna.