Jaki jest najbezpieczniejszy sposób przechodzenia przez klucze skrótu Perla?

107

Jeśli mam skrót Perla z kilkoma parami (klucz, wartość), jaka jest preferowana metoda iteracji wszystkich kluczy? Słyszałem, że używanie eachmoże w jakiś sposób mieć niezamierzone skutki uboczne. Czy to prawda i czy jedna z dwóch poniższych metod jest najlepsza, czy też jest lepszy sposób?

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

# Method 2
foreach my $key (keys(%hash)) {
    # Something
}
Rudd Zwolinski
źródło

Odpowiedzi:

199

Praktyczną zasadą jest użycie funkcji najlepiej dostosowanej do Twoich potrzeb.

Jeśli chcesz tylko klucze i nie planujesz kiedykolwiek czytać żadnej z wartości, użyj keys ():

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

Jeśli chcesz tylko wartości, użyj wartości ():

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

Jeśli potrzebujesz kluczy i wartości, użyj each ():

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

Jeśli planujesz zmienić klucze skrótu w jakikolwiek sposób, z wyjątkiem usunięcia bieżącego klucza podczas iteracji, nie możesz używać each (). Na przykład ten kod służący do tworzenia nowego zestawu kluczy z dużymi literami z podwojonymi wartościami działa dobrze przy użyciu keys ():

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

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

produkując oczekiwany wynikowy hash:

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

Ale używając each () do zrobienia tego samego:

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

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

daje nieprawidłowe wyniki w trudny do przewidzenia sposób. Na przykład:

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

Jest to jednak bezpieczne:

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

Wszystko to jest opisane w dokumentacji Perla:

% perldoc -f keys
% perldoc -f each
John Siracusa
źródło
6
Proszę dodać klucze kontekstu pustego% h; przed każdą pętlą, aby bezpiecznie wyświetlić za pomocą iteratora.
ysth
5
Z każdym z nich jest inne zastrzeżenie. Iterator jest powiązany z hashem, a nie kontekstem, co oznacza, że ​​nie jest ponownie wprowadzany. Na przykład, jeśli zapętlisz hash i wydrukujesz hash, perl wewnętrznie zresetuje iterator, tworząc pętlę kodu w nieskończoność: my% hash = (a => 1, b => 2, c => 3,); while (my ($ k, $ v) = each% hash) {print% hash; } Przeczytaj więcej na blogs.perl.org/users/rurban/2014/04/do-not-use-each.html
Rawler
28

Jedną rzeczą, o której powinieneś wiedzieć podczas używania eachjest to, że ma to efekt uboczny dodawania „stanu” do twojego skrótu (hash musi pamiętać, jaki jest klucz „następny”). Podczas korzystania z kodu, takiego jak zamieszczone powyżej fragmenty, które powtarzają cały hash za jednym razem, zwykle nie stanowi to problemu. Jednak napotkasz trudne do wyśledzenia problemy (mówię z doświadczenia;), gdy używasz eachrazem z instrukcjami, takimi jak lastlub, returnaby wyjść z while ... eachpętli, zanim przetworzysz wszystkie klucze.

W takim przypadku hash zapamięta, które klucze już zwrócił, a gdy użyjesz eachgo następnym razem (być może w całkowicie niepowiązanym fragmencie kodu), będzie kontynuowany na tej pozycji.

Przykład:

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

To drukuje:

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

Co się stało z klawiszami „bar” i baz ”? Nadal tam są, ale drugi eachzaczyna się w miejscu, w którym skończył się pierwszy, i zatrzymuje się, gdy osiąga koniec hasha, więc nigdy nie widzimy ich w drugiej pętli.

8jean
źródło
22

Miejscem, w którym eachmoże powodować problemy, jest to, że jest to prawdziwy iterator bez zakresu. Na przykład:

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

Jeśli chcesz mieć pewność, że eachpobiera wszystkie klucze i wartości, musisz upewnić się, że używasz keyslub jako valuespierwszego (ponieważ resetuje to iterator). Zobacz dokumentację dla każdego .

Darren Meyer
źródło
14

Użycie każdej składni zapobiegnie wygenerowaniu całego zestawu kluczy na raz. Może to być ważne, jeśli używasz powiązanego skrótu z bazą danych z milionami wierszy. Nie chcesz generować całej listy kluczy naraz i wyczerpać pamięć fizyczną. W tym przypadku każdy służy jako iterator, podczas gdy klucze faktycznie generują całą tablicę przed rozpoczęciem pętli.

Tak więc jedyne miejsce, w którym „każdy” jest rzeczywiste, to sytuacja, w której wartość skrótu jest bardzo duża (w porównaniu z dostępną pamięcią). Jest to prawdopodobne tylko wtedy, gdy sam hash nie żyje w pamięci, chyba że programujesz podręczne urządzenie do zbierania danych lub coś z małą pamięcią.

Jeśli pamięć nie jest problemem, zwykle paradygmat mapy lub kluczy jest bardziej rozpowszechnionym i łatwiejszym do odczytania paradygmatem.


źródło
6

Kilka różnych przemyśleń na ten temat:

  1. Nie ma nic niebezpiecznego w żadnym z iteratorów skrótu. To, co jest niebezpieczne, to modyfikowanie kluczy skrótu podczas iteracji. (Modyfikowanie wartości jest całkowicie bezpieczne.) Jedynym potencjalnym efektem ubocznym, jaki valuesprzychodzi mi do głowy, jest zwracanie aliasów, co oznacza, że ​​ich modyfikacja spowoduje zmianę zawartości skrótu. Jest to zgodne z projektem, ale w niektórych okolicznościach może nie być tym, czego chcesz.
  2. Zaakceptowana odpowiedź Johna jest dobra, z jednym wyjątkiem: dokumentacja jasno określa, że ​​nie jest bezpieczne dodawanie kluczy podczas iteracji po hashu. Może działać w przypadku niektórych zestawów danych, ale w przypadku innych zakończy się niepowodzeniem, w zależności od kolejności skrótów.
  3. Jak już wspomniano, można bezpiecznie usunąć ostatni klucz zwrócony przez each. Nie jest to prawdą w przypadku iteratora keysas eachis iterator, podczas gdy keyszwraca listę.
Michael Carman
źródło
2
Odp. „Nie dotyczy kluczy”, a raczej: nie dotyczy kluczy, a każde usunięcie jest bezpieczne. Sformułowanie, którego używasz, sugeruje, że nigdy nie jest bezpieczne usuwanie czegokolwiek podczas używania kluczy.
ysth
2
Re: „nic niebezpiecznego w żadnym z iteratorów skrótu”, innym niebezpieczeństwem jest założenie, że iterator znajduje się na początku przed rozpoczęciem każdej pętli, jak wspominają inni.
ysth
3

Zawsze używam również metody 2. Jedyną korzyścią płynącą z używania każdego z nich jest to, że jeśli tylko odczytujesz (zamiast ponownie przypisywać) wartość wpisu skrótu, nie usuwasz stale odniesienia do skrótu.

jaredg
źródło
3

Może mnie ugryzie, ale myślę, że to osobiste preferencje. Nie mogę znaleźć żadnego odniesienia w dokumentach do każdego () innego niż klucze () lub wartości () (poza oczywistą odpowiedzią „zwracają różne rzeczy”. W rzeczywistości dokumenty wskazują, że używają tego samego iteratora i wszystkie zwraca rzeczywiste wartości listy zamiast ich kopii, a modyfikowanie skrótu podczas iteracji po nim przy użyciu dowolnego wywołania jest złe.

Wszystko to powiedziawszy, prawie zawsze używam keys (), ponieważ dla mnie zwykle bardziej samodokumentuje się dostęp do wartości klucza za pośrednictwem samego skrótu. Czasami używam wartości (), gdy wartość jest odwołaniem do dużej struktury, a klucz do skrótu był już przechowywany w strukturze, w którym to momencie klucz jest zbędny i nie potrzebuję go. Myślę, że użyłem każdego () 2 razy w ciągu 10 lat programowania w Perlu i prawdopodobnie był to zły wybór za każdym razem =)

jj33
źródło
2

Zwykle używam keysi nie mogę sobie przypomnieć, kiedy ostatnio używałem lub czytałem each.

Nie zapomnij o tym map, w zależności od tego, co robisz w pętli!

map { print "$_ => $hash{$_}\n" } keys %hash;
Gary Richardson
źródło
6
nie używaj mapy, chyba że chcesz zwrócić wartość
ko-dos
-1

Powiem:

  1. Użyj tego, co jest najłatwiejsze do przeczytania / zrozumienia dla większości ludzi (więc klucze, zwykle, bym się spierał)
  2. Używaj tego, co zdecydujesz, konsekwentnie w całej bazie kodu.

Daje to 2 główne zalety:

  1. Łatwiej jest znaleźć „wspólny” kod, dzięki czemu można ponownie uwzględnić go w funkcjach / metiodach.
  2. Konserwacja jest łatwiejsza dla przyszłych programistów.

Nie sądzę, aby używanie kluczy nad każdym z nich było droższe, więc nie ma potrzeby stosowania dwóch różnych konstrukcji dla tego samego elementu w kodzie.

Hogsmill
źródło
1
Wraz ze keyswzrostem użycia pamięci o hash-size * avg-key-size. Biorąc pod uwagę, że rozmiar klucza jest ograniczona tylko przez pamięć (jak są one po prostu elementów tablicy, jak „ich” wartościami odpowiadającymi pod maską), w niektórych sytuacjach może być niewspółmiernie droższe zarówno zużycia pamięci i czasu potrzebnego do wykonania kopii.
Adrian Günter