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.
{ [] }
com<<=
o menor número de surpresas, não fosse pelo fato de que o esquecimento acidental do=
pode levar a uma sessão de depuração muito confusa.Você está especificando que o valor padrão para o hash é uma referência a esse array específico (inicialmente vazio).
Eu acho que você quer:
Isso define o valor padrão de cada chave para uma nova matriz.
fonte
Array
instâncias em cada invocação. A saber:h = Hash.new { |hash, key| hash[key] = []; puts hash[key].object_id }; h[1] # => 16348490; h[2] # => 16346570
. Além disso: se você usar a versão de bloco que define o valor ({|hash,key| hash[key] = []}
) em vez daquela que simplesmente gera o valor ({ [] }
), então você só precisa<<
, não<<=
ao adicionar elementos.O operador
+=
quando aplicado a esses hashes funciona conforme o esperado.Isso pode ser porque
foo[bar]+=baz
é um açúcar sintático, poisfoo[bar]=foo[bar]+baz
quandofoo[bar]
à direita de=
é avaliado ele retorna o objeto de valor padrão e o+
operador não o altera. A mão esquerda é o açúcar sintático para o[]=
método, que não altera o valor padrão .Note que isto não se aplica para
foo[bar]<<=baz
como ele vai ser equivalente afoo[bar]=foo[bar]<<baz
e<<
vai mudar o valor padrão .Além disso, não encontrei diferença entre
Hash.new{[]}
eHash.new{|hash, key| hash[key]=[];}
. Pelo menos em ruby 2.1.2.fonte
Hash.new{[]}
é o mesmo queHash.new([])
para mim com a falta do<<
comportamento esperado (embora, é claro,Hash.new{|hash, key| hash[key]=[];}
funcione). Coisas pequenas estranhas quebrando todas as coisas: /Quando você escreve,
você passa a referência padrão de array para todos os elementos em hash. por causa disso, todos os elementos em hash referem-se ao mesmo array.
se você quiser que cada elemento em hash se refira a uma matriz separada, você deve usar
para obter mais detalhes sobre como funciona em ruby, consulte: http://ruby-doc.org/core-2.2.0/Array.html#method-c-new
fonte
Hash.new { [] }
se não trabalhar. Veja minha resposta para detalhes. Também já é a solução proposta em outra resposta.