Primeiro, observe que esse comportamento se aplica a qualquer valor padrão que sofra mutações subsequentes (por exemplo, hashes e strings), não apenas arrays.
TL; DR : use Hash.new { |h, k| h[k] = [] }
se quiser a solução mais idiomática e não se importa por quê.
O que não funciona
Porque Hash.new([])
não funciona
Vamos analisar mais detalhadamente por Hash.new([])
que não funciona:
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 #=> {}
Podemos ver que nosso objeto padrão está sendo reutilizado e modificado (isso porque ele é passado como o único valor padrão, o hash não tem como obter um novo valor padrão novo), mas por que não há chaves ou valores na matriz, apesar de h[1]
ainda nos dar um valor? Aqui está uma dica:
h[42] #=> ["a", "b"]
A matriz retornada por cada []
chamada é apenas o valor padrão, que estamos alterando todo esse tempo, então agora contém nossos novos valores. Já <<
que não atribui ao hash (nunca pode haver atribuição em Ruby sem um =
presente † ), nunca colocamos nada em nosso hash real. Em vez disso, temos que usar <<=
(que é para <<
como +=
é +
):
h[2] <<= 'c' #=> ["a", "b", "c"]
h #=> {2=>["a", "b", "c"]}
É o mesmo que:
h[2] = (h[2] << 'c')
Porque Hash.new { [] }
não funciona
Usar Hash.new { [] }
resolve o problema de reutilizar e modificar o valor padrão original (como o bloco dado é chamado a cada vez, retornando uma nova matriz), mas não o problema de atribuição:
h = Hash.new { [] }
h[0] << 'a' #=> ["a"]
h[1] <<= 'b' #=> ["b"]
h #=> {1=>["b"]}
O que funciona
A forma de atribuição
Se nos lembrarmos de sempre usar <<=
, então Hash.new { [] }
é uma solução viável, mas é um pouco estranha e não idiomática (nunca vi ser <<=
usado em estado selvagem). Também está sujeito a erros sutis se <<
for usado inadvertidamente.
O caminho mutável
A documentação paraHash.new
estados (ênfase minha):
Se um bloco for especificado, ele será chamado com o objeto hash e a chave e deve retornar o valor padrão. É responsabilidade do bloco armazenar o valor no hash, se necessário .
Portanto, devemos armazenar o valor padrão no hash de dentro do bloco se quisermos usar em <<
vez de <<=
:
h = Hash.new { |h, k| h[k] = [] }
h[0] << 'a' #=> ["a"]
h[1] << 'b' #=> ["b"]
h #=> {0=>["a"], 1=>["b"]}
Isso move efetivamente a atribuição de nossas chamadas individuais (que usariam <<=
) para o bloco passado Hash.new
, removendo o fardo do comportamento inesperado durante o uso <<
.
Observe que existe uma diferença funcional entre este método e os outros: desta forma atribui o valor padrão na leitura (já que a atribuição sempre acontece dentro do bloco). Por exemplo:
h1 = Hash.new { |h, k| h[k] = [] }
h1[:x]
h1 #=> {:x=>[]}
h2 = Hash.new { [] }
h2[:x]
h2 #=> {}
O caminho imutável
Você pode estar se perguntando por Hash.new([])
que não funciona enquanto Hash.new(0)
funciona bem. A chave é que os Numéricos em Ruby são imutáveis, então, naturalmente, nunca acabamos por transformá-los no local. Se tratássemos nosso valor padrão como imutável, poderíamos usar Hash.new([])
muito bem também:
h = Hash.new([].freeze)
h[0] += ['a'] #=> ["a"]
h[1] += ['b'] #=> ["b"]
h[2] #=> []
h #=> {0=>["a"], 1=>["b"]}
No entanto, observe isso ([].freeze + [].freeze).frozen? == false
. Portanto, se você quiser garantir que a imutabilidade seja preservada do começo ao fim, deve tomar cuidado para congelar novamente o novo objeto.
Conclusão
De todos os caminhos, pessoalmente prefiro “o caminho imutável” - a imutabilidade geralmente torna o raciocínio sobre as coisas muito mais simples. Afinal, é o único método que não tem possibilidade de comportamento inesperado oculto ou sutil. No entanto, a forma mais comum e idiomática é “a forma mutável”.
Por fim, esse comportamento dos valores padrão do Hash é observado no Ruby Koans .
† Isso não é estritamente verdadeiro, métodos como instance_variable_set
ignorar isso, mas devem existir para metaprogramação, pois o valor l em =
não pode ser dinâmico.