Was ist der sicherste Weg, um die Schlüssel eines Perl-Hash zu durchlaufen?

107

Wenn ich einen Perl-Hash mit einer Reihe von (Schlüssel-, Wert-) Paaren habe, was ist die bevorzugte Methode zum Durchlaufen aller Schlüssel? Ich habe gehört, dass die Verwendung eachin irgendeiner Weise unbeabsichtigte Nebenwirkungen haben kann. Also, ist das wahr und ist eine der beiden folgenden Methoden die beste oder gibt es einen besseren Weg?

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

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

Antworten:

199

Als Faustregel gilt, die Funktion zu verwenden, die Ihren Anforderungen am besten entspricht.

Wenn Sie nur die Schlüssel möchten und nicht vorhaben, jemals einen der Werte zu lesen , verwenden Sie keys ():

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

Wenn Sie nur die Werte möchten, verwenden Sie values ​​():

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

Wenn Sie die Schlüssel und Werte benötigen , verwenden Sie each ():

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

Wenn Sie vorhaben, die Schlüssel des Hashs in irgendeiner Weise zu ändern, außer den aktuellen Schlüssel während der Iteration zu löschen, dürfen Sie nicht jeden () verwenden. Dieser Code zum Erstellen eines neuen Satzes von Großbuchstaben mit doppelten Werten funktioniert beispielsweise mit keys ():

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

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

Erzeugen des erwarteten resultierenden Hash:

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

Aber mit jedem () das Gleiche tun:

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

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

führt auf schwer vorhersehbare Weise zu falschen Ergebnissen. Beispielsweise:

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

Dies ist jedoch sicher:

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

All dies ist in der Perl-Dokumentation beschrieben:

% perldoc -f keys
% perldoc -f each
John Siracusa
quelle
6
Bitte fügen Sie einen ungültigen Kontextschlüssel% h hinzu. vor jeder Schleife mit dem Iterator sicher anzeigen.
ysth
5
Es gibt eine weitere Einschränkung bei jedem. Der Iterator ist an den Hash gebunden, nicht an den Kontext, was bedeutet, dass er nicht erneut eintritt. Wenn Sie beispielsweise einen Hash durchlaufen und den Hash-Perl drucken, wird der Iterator intern zurückgesetzt, wodurch diese Code-Schleife endlos wird: my% hash = (a => 1, b => 2, c => 3,); while (my ($ k, $ v) = jeder% Hash) {print% Hash; } Lesen Sie mehr unter blogs.perl.org/users/rurban/2014/04/do-not-use-each.html
Rawler
28

Eine Sache, die Sie bei der Verwendung beachten sollten, eachist, dass sie den Nebeneffekt hat, Ihrem Hash "state" hinzuzufügen (der Hash muss sich merken, was der "nächste" Schlüssel ist). Wenn Sie Code wie die oben angegebenen Snippets verwenden, die den gesamten Hash auf einmal durchlaufen, ist dies normalerweise kein Problem. Es wird jedoch schwierig sein, Probleme aufzuspüren (ich spreche aus Erfahrung;), wenn Sie eachzusammen mit Anweisungen wie lastoder returndie while ... eachSchleife verlassen, bevor Sie alle Schlüssel verarbeitet haben.

In diesem Fall merkt sich der Hash, welche Schlüssel er bereits zurückgegeben hat, und wenn Sie eachihn das nächste Mal verwenden (möglicherweise in einem völlig unabhängigen Code), wird er an dieser Position fortgesetzt.

Beispiel:

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

Dies druckt:

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

Was ist mit den Tasten "bar" und "baz" passiert? Sie sind immer noch da, aber die zweite eachbeginnt dort, wo die erste aufgehört hat, und endet, wenn sie das Ende des Hashs erreicht, sodass wir sie in der zweiten Schleife nie sehen.

8jean
quelle
22

Der Ort, an dem eachSie Probleme verursachen können, ist, dass es sich um einen echten Iterator ohne Gültigkeitsbereich handelt. Als Beispiel:

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

Wenn Sie sicherstellen müssen, dass eachalle Schlüssel und Werte abgerufen werden, müssen Sie sicherstellen, dass Sie keysoder valueszuerst verwenden (da dies den Iterator zurücksetzt). Siehe jeweils die Dokumentation .

Darren Meyer
quelle
14

Durch die Verwendung der einzelnen Syntax wird verhindert, dass der gesamte Schlüsselsatz auf einmal generiert wird. Dies kann wichtig sein, wenn Sie einen verknüpften Hash an eine Datenbank mit Millionen von Zeilen verwenden. Sie möchten nicht die gesamte Liste der Schlüssel auf einmal generieren und Ihren physischen Speicher erschöpfen. In diesem Fall dient jeder als Iterator, während Schlüssel tatsächlich das gesamte Array generieren, bevor die Schleife beginnt.

Der einzige Ort, an dem "jeder" wirklich von Nutzen ist, ist, wenn der Hash sehr groß ist (im Vergleich zum verfügbaren Speicher). Dies ist nur dann wahrscheinlich, wenn der Hash selbst nicht im Speicher selbst gespeichert ist, es sei denn, Sie programmieren ein Handheld-Datenerfassungsgerät oder etwas mit kleinem Speicher.

Wenn das Gedächtnis kein Problem darstellt, ist normalerweise das Karten- oder Schlüsselparadigma das umfassendere und leichter zu lesende Paradigma.


quelle
6

Einige verschiedene Gedanken zu diesem Thema:

  1. An keinem der Hash-Iteratoren selbst ist etwas Unsicheres. Was unsicher ist, ist das Ändern der Schlüssel eines Hashs, während Sie darüber iterieren. (Es ist absolut sicher, die Werte zu ändern.) Der einzige mögliche Nebeneffekt, den ich mir valuesvorstellen kann, ist, dass Aliase zurückgegeben werden, was bedeutet, dass durch Ändern der Werte der Inhalt des Hashs geändert wird. Dies ist beabsichtigt, kann aber unter bestimmten Umständen nicht das sein, was Sie wollen.
  2. Johns akzeptierte Antwort ist mit einer Ausnahme gut: Aus der Dokumentation geht hervor, dass es nicht sicher ist, Schlüssel hinzuzufügen, während ein Hash durchlaufen wird. Es kann für einige Datensätze funktionieren, für andere jedoch je nach Hash-Reihenfolge fehlschlagen.
  3. Wie bereits erwähnt, ist es sicher, den zuletzt von zurückgegebenen Schlüssel zu löschen each. Das ist nicht wahr für keysso eachist ein Iterator während keyskehrt eine Liste.
Michael Carman
quelle
2
Betreff "nicht wahr für Schlüssel", sondern: Es gilt nicht für Schlüssel und jedes Löschen ist sicher. Die von Ihnen verwendete Formulierung impliziert, dass es niemals sicher ist, etwas zu löschen, wenn Sie Schlüssel verwenden.
ysth
2
Betreff: "Nichts unsicheres an einem der Hash-Iteratoren", die andere Gefahr besteht darin, dass der Iterator am Anfang steht, bevor eine einzelne Schleife gestartet wird, wie andere erwähnen.
ysth
3

Ich benutze immer auch Methode 2. Der einzige Vorteil der Verwendung ist, dass Sie den Hash nicht ständig de-referenzieren, wenn Sie nur den Wert des Hash-Eintrags lesen (anstatt ihn neu zuzuweisen).

jaredg
quelle
3

Ich werde vielleicht von diesem gebissen, aber ich denke, dass es eine persönliche Präferenz ist. Ich kann in den Dokumenten keinen Verweis darauf finden, dass sich jedes () von den Schlüsseln () oder Werten () unterscheidet (abgesehen von der offensichtlichen Antwort "Sie geben verschiedene Dinge zurück"). Tatsächlich geben die Dokumente an, dass derselbe Iterator und alle verwendet werden Geben Sie tatsächliche Listenwerte anstelle von Kopien davon zurück, und es ist schlecht, den Hash zu ändern, während Sie ihn mit einem beliebigen Aufruf durchlaufen.

Trotzdem verwende ich fast immer keys (), da es für mich normalerweise selbstdokumentierender ist, über den Hash selbst auf den Wert des Schlüssels zuzugreifen. Ich verwende gelegentlich values ​​(), wenn der Wert auf eine große Struktur verweist und der Schlüssel zum Hash bereits in der Struktur gespeichert war. Zu diesem Zeitpunkt ist der Schlüssel redundant und ich brauche ihn nicht. Ich glaube, ich habe in 10 Jahren Perl-Programmierung jedes () 2 Mal verwendet und es war wahrscheinlich beide Male die falsche Wahl =)

jj33
quelle
2

Normalerweise benutze keysich und ich kann mir nicht vorstellen, wann ich das letzte Mal benutzt oder gelesen habe each.

Vergessen Sie nicht map, je nachdem, was Sie in der Schleife tun!

map { print "$_ => $hash{$_}\n" } keys %hash;
Gary Richardson
quelle
6
Verwenden Sie keine Karte, es sei denn, Sie möchten den Rückgabewert
ko-dos
-1

Ich würde sagen:

  1. Verwenden Sie das, was für die meisten Menschen am einfachsten zu lesen / zu verstehen ist (also Schlüssel, würde ich normalerweise argumentieren)
  2. Verwenden Sie alles, was Sie entscheiden, konsequent über die gesamte Codebasis.

Dies ergibt 2 Hauptvorteile:

  1. Es ist einfacher, "allgemeinen" Code zu erkennen, damit Sie Funktionen / Methoden neu berücksichtigen können.
  2. Für zukünftige Entwickler ist die Wartung einfacher.

Ich denke nicht, dass es teurer ist, Schlüssel über jedem zu verwenden, so dass Sie nicht zwei verschiedene Konstrukte für dasselbe in Ihrem Code benötigen.

Hogsmill
quelle
1
Mit der keysSpeichernutzung steigt um hash-size * avg-key-size. Da die Schlüsselgröße nur durch den Speicher begrenzt ist (da es sich lediglich um Array-Elemente wie "ihre" entsprechenden Werte unter der Haube handelt), kann dies in einigen Situationen sowohl bei der Speichernutzung als auch bei der Erstellung der Kopie unerschwinglich sein .
Adrian Günter