Chyba najczęstszy błąd, jaki zdarza mi się spotykać w kodzie perlowym, wygląda tak:
while( my ($key, $value) = each %CONFIG ) {
process_cfg_item($key, $value);
}
albo tak:
sub find_items {
my ($items_dict, $pfx) = @_;
my @reply;
while( my ($key, $value) = each %$items_dict ) {
if ( $key eq $pfx ) {
return ($value); # exact match
}
if ( $key =~ /^$key_re/ ) {
push @reply, $value;
}
}
return @reply;
}
Może też być pętla zawierająca break
.
Taki kod często dobrze działa przy testowym uruchomieniu i ... generuje mistyczne dziwne zachowania później.
Problem polega na możliwości przerwania iteracji each
na słowniku przed wyczerpaniem wszystkich elementów (w pierwszym przykładzie takie przerwanie nastąpi jeśli wołana procedura rzuci wyjątek, który zostanie obsłużony gdzieś wyżej).
Gdy po takim przerwaniu jakaś procedura (może ta sama, może inna) zrobi później each
na tym samym słowniku - perl potraktuje to jako kontynuację tej samej pętli i przeczyta jedynie pozostałe elementy. Zresztą - nie wiadomo które (kolejność jest niedeterministyczna i zmienna).
Dzieje się tak, bo aktualny wskaźnik each
stanowi ukryty atrybut słownika, a nie zmienną lokalną. Pętla while (($k,$v) = each %dict)
niczego nie zaczyna, po prostu przestawia ten ukryty atrybut na następny element (a po dojściu do końca, przestawia go na pierwszy).
Można to próbować obchodzić tak:
each %dict;
while (my($key, $value) = each %dict) {
# ...
}
(bezkontekstowe each
resetuje wskaźnik) ale to też nie zawsze działa. Nie obsłuży
nam przypadku, gdy dwie procedury spróbują robić each
równocześnie (np. funkcja robiąca each
po jakimś globalnym słowniku zawoła z wnętrza pętli inną funkcję, która zrobi each
po tym samym słowniku).
Wniosek?
Słownikowe each
należy stosować tylko do słowników używanych wyłącznie w ramach jednej funkcji. Dla słowników globalnych a także dla słowników przekazywanych czy zwracanych przez referencję do innych procedur, używanie tej konstrukcji jest proszeniem się o kłopoty. Bezpieczne jest nieznacznie mniej wydajne:
foreach my $key (keys %dict) {
my $value = $dict{$key};
# ...
}