Qual é a maneira mais segura de iterar pelas chaves de um hash Perl?

107

Se eu tiver um hash Perl com vários pares de (chave, valor), qual é o método preferido de iteração por todas as chaves? Ouvi dizer que o uso eachpode, de alguma forma, ter efeitos colaterais indesejados. Então, isso é verdade, e um dos dois métodos a seguir é o melhor ou existe uma maneira melhor?

# Method 1
while (my ($key, $value) = each(%hash)) {
    # Something
}

# Method 2
foreach my $key (keys(%hash)) {
    # Something
}
Rudd Zwolinski
fonte

Respostas:

199

A regra é usar a função mais adequada às suas necessidades.

Se você deseja apenas as chaves e não planeja ler nenhum dos valores, use keys ():

foreach my $key (keys %hash) { ... }

Se você quiser apenas os valores, use values ​​():

foreach my $val (values %hash) { ... }

Se você precisar das chaves e dos valores, use each ():

keys %hash; # reset the internal iterator so a prior each() doesn't affect the loop
while(my($k, $v) = each %hash) { ... }

Se você planeja alterar as chaves do hash de qualquer forma, exceto para excluir a chave atual durante a iteração, você não deve usar each (). Por exemplo, este código para criar um novo conjunto de chaves maiúsculas com valores duplicados funciona bem usando keys ():

%h = (a => 1, b => 2);

foreach my $k (keys %h)
{
  $h{uc $k} = $h{$k} * 2;
}

produzindo o hash resultante esperado:

(a => 1, A => 2, b => 2, B => 4)

Mas usando each () para fazer a mesma coisa:

%h = (a => 1, b => 2);

keys %h;
while(my($k, $v) = each %h)
{
  $h{uc $k} = $h{$k} * 2; # BAD IDEA!
}

produz resultados incorretos de maneiras difíceis de prever. Por exemplo:

(a => 1, A => 2, b => 2, B => 8)

Isso, no entanto, é seguro:

keys %h;
while(my($k, $v) = each %h)
{
  if(...)
  {
    delete $h{$k}; # This is safe
  }
}

Tudo isso é descrito na documentação do perl:

% perldoc -f keys
% perldoc -f each
João siracusa
fonte
6
Adicione chaves de contexto vazio% h; antes de cada loop para mostrar com segurança usando o iterador.
ysth 01 de
5
Há outra ressalva com cada um. O iterador está vinculado ao hash, não ao contexto, o que significa que ele não é reentrante. Por exemplo, se você fizer um loop em um hash e imprimir o hash, perl irá redefinir internamente o iterador, fazendo com que este código faça um loop infinito: my% hash = (a => 1, b => 2, c => 3,); enquanto (meu ($ k, $ v) = cada% hash) {print% hash; } Leia mais em blogs.perl.org/users/rurban/2014/04/do-not-use-each.html
Rawler
28

Uma coisa que você deve estar ciente ao usar eaché que ela tem o efeito colateral de adicionar "estado" ao seu hash (o hash precisa lembrar qual é a "próxima" chave). Ao usar o código como os snippets postados acima, que iteram todo o hash de uma vez, isso geralmente não é um problema. No entanto, você encontrará problemas difíceis de rastrear (falo por experiência própria;), ao usar eachjunto com instruções como lastou returnpara sair do while ... eachloop antes de processar todas as chaves.

Nesse caso, o hash lembrará quais chaves já retornou e, quando você usá each-lo da próxima vez (talvez em um código totalmente não relacionado), ele continuará nesta posição.

Exemplo:

my %hash = ( foo => 1, bar => 2, baz => 3, quux => 4 );

# find key 'baz'
while ( my ($k, $v) = each %hash ) {
    print "found key $k\n";
    last if $k eq 'baz'; # found it!
}

# later ...

print "the hash contains:\n";

# iterate over all keys:
while ( my ($k, $v) = each %hash ) {
    print "$k => $v\n";
}

Isso imprime:

found key bar
found key baz
the hash contains:
quux => 4
foo => 1

O que aconteceu com as chaves "bar" e baz "? Elas ainda estão lá, mas a segunda eachcomeça onde a primeira parou e para quando chega ao final do hash, então nunca as vemos no segundo loop.

8jean
fonte
22

O que eachpode causar problemas é que ele é um iterador verdadeiro, sem escopo. A título de exemplo:

while ( my ($key,$val) = each %a_hash ) {
    print "$key => $val\n";
    last if $val; #exits loop when $val is true
}

# but "each" hasn't reset!!
while ( my ($key,$val) = each %a_hash ) {
    # continues where the last loop left off
    print "$key => $val\n";
}

Se você precisa ter certeza de que eachobtém todas as chaves e valores, precisa ter certeza de usar keysou valuesprimeiro (pois isso redefine o iterador). Veja a documentação de cada um .

Darren Meyer
fonte
14

Usar a sintaxe each evitará que todo o conjunto de chaves seja gerado de uma vez. Isso pode ser importante se você estiver usando um hash vinculado a um banco de dados com milhões de linhas. Você não deseja gerar toda a lista de chaves de uma vez e esgotar sua memória física. Nesse caso, cada um serve como um iterador, enquanto as chaves, na verdade, geram todo o array antes do início do loop.

Portanto, o único lugar em que "cada" tem uso real é quando o hash é muito grande (em comparação com a memória disponível). É provável que isso aconteça apenas quando o próprio hash não viver na memória, a menos que você esteja programando um dispositivo portátil de coleta de dados ou algo com pouca memória.

Se a memória não for um problema, geralmente o paradigma do mapa ou das chaves é o paradigma mais predominante e mais fácil de ler.


fonte
6

Algumas idéias diversas sobre este tópico:

  1. Não há nada inseguro em nenhum dos próprios iteradores de hash. O que não é seguro é modificar as chaves de um hash enquanto você faz a iteração sobre ele. (É perfeitamente seguro modificar os valores.) O único efeito colateral potencial que posso pensar é que valuesretorna aliases, o que significa que modificá-los irá modificar o conteúdo do hash. Isso ocorre por design, mas pode não ser o que você deseja em algumas circunstâncias.
  2. A resposta aceita de John é boa, com uma exceção: a documentação é clara que não é seguro adicionar chaves durante a iteração em um hash. Pode funcionar para alguns conjuntos de dados, mas falhará para outros, dependendo da ordem do hash.
  3. Como já observado, é seguro excluir a última chave retornada por each. Isso não é verdade para keysas eaché um iterador enquanto keysretorna uma lista.
Michael Carman
fonte
2
Re "não é verdade para chaves", ao contrário: não é aplicável a chaves e qualquer exclusão é segura. A frase que você usa indica que nunca é seguro excluir nada ao usar chaves.
ysth 01 de
2
Re: "nada inseguro em nenhum dos iteradores de hash", o outro perigo é assumir que o iterador está no início antes de iniciar cada loop, como outros mencionam.
ysth 01 de
3

Eu sempre uso o método 2 também. O único benefício de usar cada um é se você estiver apenas lendo (em vez de reatribuir) o valor da entrada de hash, você não está constantemente desreferenciando o hash.

Jaredg
fonte
3

Posso ser mordido por este, mas acho que é uma preferência pessoal. Não consigo encontrar nenhuma referência nos documentos para cada () ser diferente de keys () ou valores () (além da resposta óbvia "eles retornam coisas diferentes". Na verdade, os documentos declaram que se use o mesmo iterador e todos eles retorna valores de lista reais em vez de cópias deles, e que modificar o hash enquanto iterando sobre ele usando qualquer chamada é ruim.

Dito isso, quase sempre uso keys () porque, para mim, geralmente é mais autodocumentado acessar o valor da chave por meio do próprio hash. Ocasionalmente, uso values ​​() quando o valor é uma referência a uma grande estrutura e a chave para o hash já estava armazenada na estrutura, ponto em que a chave é redundante e eu não preciso dela. Acho que usei each () 2 vezes em 10 anos de programação Perl e provavelmente foi a escolha errada nas duas vezes =)

jj33
fonte
2

Eu costumo usar keyse não consigo pensar na última vez que usei ou li um uso de each.

Não se esqueça map, dependendo do que você está fazendo no loop!

map { print "$_ => $hash{$_}\n" } keys %hash;
Gary Richardson
fonte
6
não use o mapa, a menos que queira o valor de retorno
ko-dos
-1

Eu diria:

  1. Use o que for mais fácil de ler / entender para a maioria das pessoas (chaves, geralmente, eu diria)
  2. Use o que você decidir de forma consistente em toda a base de código.

Isso dá duas vantagens principais:

  1. É mais fácil localizar o código "comum" para que você possa refatorar em funções / métodos.
  2. É mais fácil para futuros desenvolvedores manter.

Não acho que seja mais caro usar chaves em vez de cada uma, então não há necessidade de duas construções diferentes para a mesma coisa em seu código.

Hogsmill
fonte
1
Com keyso uso de memória aumenta em hash-size * avg-key-size. Dado que o tamanho da chave é limitado apenas pela memória (já que são apenas elementos de array como "seus" valores correspondentes sob o capô), em algumas situações pode ser proibitivamente mais caro no uso de memória e no tempo gasto para fazer a cópia.
Adrian Günter