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

107

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

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

# Method 2
foreach my $key (keys(%hash)) {
    # Something
}
Радд Зволински
источник

Ответы:

199

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

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

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
Джон Сиракуза
источник
6
Пожалуйста, добавьте ключи пустого контекста% h; перед каждым циклом, чтобы безопасно показать его с помощью итератора.
ysth 01
5
С каждым есть еще одно предостережение. Итератор привязан к хешу, а не к контексту, что означает, что он не является реентерабельным. Например, если вы перебираете хэш и распечатываете хеш, perl внутренне сбрасывает итератор, делая этот цикл кода бесконечным: my% hash = (a => 1, b => 2, c => 3,); while (my ($ k, $ v) = каждый% хэш) {print% hash; } Подробнее читайте на blogs.perl.org/users/rurban/2014/04/do-not-use-each.html
Rawler
28

Одна вещь, о которой вы должны знать при использовании, 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начинается там, где остановился первый, и останавливается, когда достигает конца хэша, поэтому мы никогда не видим их во втором цикле.

8джин
источник
22

Место , где eachможет вызвать проблемы в том , что это правда, не Scoped итератора. В качестве примера:

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

Если вам нужно быть уверенным, что он eachполучает все ключи и значения, вам нужно убедиться, что вы сначала используете keysили values(так как это сбрасывает итератор). См. Документацию для каждого .

Даррен Мейер
источник
14

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

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

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


источник
6

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

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

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

Джаред
источник
3

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

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

jj33
источник
2

Я обычно использую keysи не могу вспомнить, когда в последний раз использовал или читал об использованииeach .

Не забывайте map, в зависимости от того, что вы делаете в цикле!

map { print "$_ => $hash{$_}\n" } keys %hash;
Гэри Ричардсон
источник
6
не используйте карту, если вам не нужно возвращаемое значение
ko-dos
-1

Я скажу:

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

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

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

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

Hogsmill
источник
1
При keysиспользовании памяти увеличивается на hash-size * avg-key-size. Учитывая, что размер ключа ограничен только памятью (поскольку они просто элементы массива, такие как «их» соответствующие значения под капотом), в некоторых ситуациях это может быть недопустимо дороже как с точки зрения использования памяти, так и с точки зрения времени, затрачиваемого на создание копии.
Адриан Гюнтер