Могу ли я скопировать хэш без сброса его "каждого" итератора?

Я использую each для итерации через хеш Perl:

while (my ($key,$val) = each %hash) {
   ...
}

Затем происходит что-то интересное, и я хочу распечатать хэш. Сначала я считаю что-то вроде:

while (my ($key,$val) = each %hash) {
   if (something_interesting_happens()) {
      foreach my $k (keys %hash) { print "$k => $hash{$k}\n" }
   }
}

Но это не сработает, потому что все знают, что вызов keys (или values) хэша сбрасывает внутренний итератор, используемый для each, и мы можем получить бесконечный цикл. Например, эти скрипты будут работать вечно:

perl -e '%a=(foo=>1); while(each %a){keys %a}'
perl -e '%a=(foo=>1); while(each %a){values %a}'

Нет проблем, подумал я. Я мог бы сделать копию хэша и распечатать копию.

   if (something_interesting_happens()) {
      %hash2 = %hash;
      foreach my $k (keys %hash2) { print "$k => $hash2{$k}\n" }
   }

Но это тоже не работает. Это также сбрасывает итератор each. Фактически любое использование %hash в контексте списка кажется reset его итератором each. Так что они тоже бегут:

perl -e '%a=(foo=>1); while(each %a){%b = %a}'
perl -e '%a=(foo=>1); while(each %a){@b = %a}'
perl -e '%a=(foo=>1); while(each %a){print %a}'

Насколько это документировано? Имеет смысл, что perl, возможно, потребуется использовать один и тот же внутренний итератор, чтобы направить содержимое хэша в стек возврата, но я также могу представить хэш-реализации, которые не нуждались в этом.

Что еще более важно, есть ли способ сделать то, что я хочу? Чтобы получить все элементы хэша без сброса итератора each?


Это также говорит о том, что вы не можете отлаживать хэш внутри итерации each. Рассмотрите возможность запуска отладчика:

%a = (foo => 123, bar => 456);
while ( ($k,$v) = each %a ) {
    $DB::single = 1;
    $o .= "$k,$v;";
}
print $o;

Просто просмотрев хэш, где останавливается отладчик (например, набрав p %a или x %a), вы измените вывод программы.


Обновление: Я загрузил Hash::SafeKeys как общее решение этой проблемы. Спасибо @gpojd за то, что указали мне в правильном направлении и @cjm за предложение, которое сделало решение намного проще.

Ответы

Ответ 1

Вы пробовали Storable dclone, чтобы скопировать его? Вероятно, это будет примерно так:

use Storable qw(dclone);
my %hash_copy = %{ dclone( \%hash ) };

Ответ 2

Насколько велика эта хэш? Сколько времени требуется для прохождения через него, чтобы вы заботились о сроках доступа?

Просто установите флаг и выполните действие после окончания итерации:

my $print_it;
while (my ($key,$val) = each %hash) {
    $print_it = 1 if something_interesting_happens();
    ...
}

if ($print_it) {
    foreach my $k (keys %hash) { print "$k => $hash{$k}\n" }
}

Хотя нет причин не использовать each в коде распечатки, если вы не планируете сортировку по ключу или что-то в этом роде.

Ответ 3

Не забывайте, что keys %hash уже определен при вводе цикла while. Можно было просто сохранить ключи в массив для последующего использования:

my @keys = keys %hash;

while (my ($key,$val) = each %hash) {

    if (something_interesting_happens()) {

        print "$_ => $hash{$_}\n" for @keys;
    }
}

Даунсайд:

  • Это менее элегантный (субъективный)
  • Это не сработает, если %hash будет изменен (но тогда зачем использовать each в первую очередь?)

Потенциал роста:

  • Он использует меньше памяти, избегая хеш-копирования

Ответ 4

Не совсем. each невероятно хрупкий. Он сохраняет итерационное состояние на итерированном хеше, состояние, которое повторно используется другими частями perl, когда оно в этом нуждается. Гораздо безопаснее забыть, что он существует, и всегда перебирает свой собственный список из результата keys %hash, потому что состояние итерации по списку хранится лексически как часть самого цикла for, поэтому оно не защищено от другие вещи.