Какой самый безопасный способ перебирать ключи хэша Perl?

Если у меня есть хеш Perl с кучей пар (ключ, значение), какой метод перебора всех ключей предпочтительнее? Я слышал, что использование each может каким-то образом иметь непредвиденные побочные эффекты. Итак, так ли это, и является ли один из двух следующих методов лучшим или есть лучший способ?

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

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

Ответов (9)

Решение

Практическое правило - использовать функцию, наиболее подходящую для ваших нужд.

Если вам просто нужны ключи и вы не планируете когда-либо читать какие-либо значения, используйте keys ():

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

Если вам просто нужны значения, используйте values ​​():

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

Если вам нужны ключи и значения, используйте each ():

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

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

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

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

создание ожидаемого результирующего хеша:

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

Но используя each (), чтобы сделать то же самое:

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

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

дает неверные результаты труднопредсказуемым образом. Например:

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

Однако это безопасно:

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

Все это описано в документации perl:

% perldoc -f keys
% perldoc -f each

I usually use keys and I can't think of the last time I used or read a use of each .

Don't forget about map, depending on what you're doing in the loop!

map { print "$_ => $hash{$_}\n" } keys %hash;

Несколько разных мыслей по этой теме:

  1. В самих итераторах хеширования нет ничего опасного. Что небезопасно, так это изменение ключей хэша во время его итерации. (Изменять значения совершенно безопасно.) Единственный потенциальный побочный эффект, о котором я могу думать, - это valuesвозвращение псевдонимов, что означает, что их изменение приведет к изменению содержимого хэша. Это сделано намеренно, но в некоторых случаях это может быть не то, что вам нужно.
  2. Принятый ответ Джона хорош с одним исключением: в документации ясно сказано, что добавлять ключи во время итерации по хешу небезопасно. Это может работать для некоторых наборов данных, но не работать для других в зависимости от порядка хеширования.
  3. Как уже отмечалось, можно безопасно удалить последний ключ, возвращенный each. Это не верно для , keysкак eachэто итератор , а keysвозвращает список.

Я также всегда использую метод 2. Единственное преимущество использования каждого из них заключается в том, что если вы просто читаете (а не повторно назначаете) значение записи хэша, вы не будете постоянно разыменовывать хэш.

Меня это может укусить, но я думаю, что это личное предпочтение. Я не могу найти никаких ссылок в документах на то, что each () отличается от keys () или values ​​() (кроме очевидного ответа «они возвращают разные вещи». На самом деле в документах указано, что используется один и тот же итератор, и все они возвращать фактические значения списка вместо их копий, и то, что изменение хэша во время итерации по нему с использованием любого вызова - это плохо.

При этом я почти всегда использую keys (), потому что для меня обычно более самодокументируется доступ к значению ключа через сам хеш. Я иногда использую values ​​(), когда значение является ссылкой на большую структуру, а ключ к хешу уже был сохранен в структуре, после чего ключ является избыточным и мне он не нужен. Думаю, я использовал each () 2 раза за 10 лет программирования на Perl, и, вероятно, оба раза это был неправильный выбор =)

Использование синтаксиса each предотвратит одновременное создание всего набора ключей. Это может быть важно, если вы используете связанный хэш с базой данных с миллионами строк. Вы же не хотите создавать сразу весь список ключей и исчерпывать свою физическую память. В этом случае каждый служит итератором, тогда как ключи фактически генерируют весь массив перед запуском цикла.

Таким образом, единственное место, где "каждый" действительно используется, - это когда хэш очень большой (по сравнению с доступной памятью). Это может произойти только в том случае, если сам хеш не находится в памяти, если только вы не программируете портативное устройство сбора данных или что-то с небольшим объемом памяти.

Если память не является проблемой, обычно парадигма карты или ключей является более распространенной и простой для чтения парадигмой.

Одна вещь, о которой вы должны знать при использовании, each- это то, что у нее есть побочный эффект добавления «состояния» к вашему хешу (хеш должен помнить, что такое «следующий» ключ). При использовании кода, подобного приведенным выше фрагментам, который перебирает весь хеш за один раз, это обычно не проблема. Однако вы столкнетесь с трудно обнаруживаемыми проблемами (я говорю по опыту;) при использовании each вместе с такими операторами, как last или return для выхода из while ... each цикла до того, как вы обработали все ключи.

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

Пример:

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";
}

Это печатает:

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

Что случилось с клавишами bar и baz? Они все еще там, но второй each начинается там, где остановился первый, и останавливается, когда достигает конца хэша, поэтому мы никогда не видим их во втором цикле.

The place where each can cause you problems is that it's a true, non-scoped iterator. By way of example:

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";
}

If you need to be sure that each gets all the keys and values, you need to make sure you use keys or values first (as that resets the iterator). See the documentation for each.

Я скажу:

  1. Используйте то, что наиболее легко читать / понимать для большинства людей (так что ключи, как правило, я бы поспорил)
  2. Используйте все, что вы решите, последовательно во всей кодовой базе.

Это дает 2 основных преимущества:

  1. Легче обнаружить «общий» код, чтобы его можно было преобразовать в функции / метиоды.
  2. Будущим разработчикам проще поддерживать.

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