Comportamento estranho e inesperado (desaparecendo / alterando valores) ao usar o valor padrão Hash, por exemplo, Hash.new ()

Considere este código:

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

Tudo bem, mas:

 h = Hash.new([]) # Empty array as default value h[1] < {1=>[1]} ← Ok h[2] < {1=>[1,2], 2=>[1,2]} ← Why did `1` change? h[3] < {1=>[1,2,3], 2=>[1,2,3]} ← Where is `3`? 

Neste ponto, espero que o hash seja:

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

mas está longe disso. O que está acontecendo e como posso obter o comportamento que espero?

    Primeiro, observe que esse comportamento se aplica a qualquer valor padrão que seja subsequentemente mutado (por exemplo, hashes e sequências de caracteres) e não apenas matrizes.

    TL; DR : Use Hash.new { |h, k| h[k] = [] } Hash.new { |h, k| h[k] = [] } se você quiser a solução mais simples e mais idiomática.


    O que não funciona

    Por que Hash.new([]) não funciona

    Vamos ver mais a fundo por que o Hash.new([]) 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 object padrão está sendo reutilizado e mutado (isso é porque ele é passado como o único valor padrão, o hash não tem como obter um novo valor padrão), 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 sofrendo mutação todo esse tempo, então agora contém nossos novos valores. Como < < 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 += é para + ):

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

    Isso é o mesmo que:

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

    Por que Hash.new { [] } não funciona

    O uso de Hash.new { [] } soluciona o problema de reutilizar e alterar o valor padrão original (como o bloco fornecido é chamado a cada vez, retornando um novo array), 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

    O caminho de atribuição

    Se nos lembrarmos de sempre usar < <= , então Hash.new { [] } é uma solução viável, mas é um pouco estranho e não-idiomático (nunca vi < <= usado na natureza). Também é propenso a erros sutis se < < é inadvertidamente usado.

    O caminho mutável

    A documentação para os estados Hash.new (ênfase minha):

    Se um bloco for especificado, ele será chamado com o object hash e a chave e deverá retornar o valor padrão. É de 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 < < vez de < <= :

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

    Isso efetivamente move a atribuição de nossas chamadas individuais (o que usaria < <= ) para o bloco passado para Hash.new , removendo a carga de comportamento inesperado ao usar < < .

    Note que há uma diferença funcional entre este método e os outros: desta forma, atribui o valor padrão na leitura (como 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 #=> {} 

    A maneira imutável

    Você pode estar se perguntando por que o Hash.new([]) não funciona enquanto o Hash.new(0) funciona bem. A chave é que os Numerics em Ruby são imutáveis, então, naturalmente, nunca acabamos modificando-os no local. Se nós tratássemos nosso valor padrão como imutável, poderíamos usar Hash.new([]) 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 que ([].freeze + [].freeze).frozen? == false ([].freeze + [].freeze).frozen? == false . Então, se você quer garantir que a imutabilidade seja preservada por toda parte, então você deve tomar cuidado para voltar a congelar o novo object.

    De todas as maneiras, eu pessoalmente prefiro assim - a imutabilidade geralmente torna o raciocínio sobre as coisas muito mais simples (afinal, esse é o único método que não tem possibilidade de comportamento inesperado oculto ou sutil).


    Isso não é estritamente verdadeiro, methods como instance_variable_set ignoram isso, mas eles devem existir para a metaprogramação, pois o valor de l em = não pode ser dynamic.

    Quando você chama Hash.new([]) , o valor padrão para qualquer chave não é apenas uma matriz vazia, é a mesma matriz vazia.

    Para criar uma nova matriz para cada valor padrão, use o formulário de bloco do construtor:

     Hash.new { [] } 

    Você está especificando que o valor padrão do hash é uma referência a essa matriz específica (inicialmente vazia).

    Eu acho que você quer:

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

    Isso define o valor padrão de cada chave para uma nova matriz.

    O operador += quando aplicado a esses hashes funciona como esperado.

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

    Isso pode ser porque foo[bar]+=baz é açúcar sintático para foo[bar]=foo[bar]+baz quando foo[bar] à direita de = é avaliado, retorna o object de valor padrão e o operador + não mude. 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 a foo[bar]< <=baz , pois será equivalente a foo[bar]=foo[bar]< e < < mudará o valor padrão .

    Além disso, não encontrei nenhuma diferença entre Hash.new{[]} e Hash.new{|hash, key| hash[key]=[];} Hash.new{|hash, key| hash[key]=[];} Pelo menos em ruby 2.1.2.

    Quando você escreve

     h = Hash.new([]) 

    você passa referência padrão de array para todos os elementos em hash. por causa disso todos os elementos em hash se referem a mesma matriz.

    se você quiser que cada elemento em hash se refira a array separado, você deve usar

     h = Hash.new{[]} 

    Para mais detalhes de como funciona em Ruby, por favor, vá até o seguinte: http://ruby-doc.org/core-2.2.0/Array.html#method-c-new