it-swarm.com.de

Wie funktioniert PHP 'foreach' eigentlich?

Lassen Sie mich dem voranstellen, indem Sie sagen, dass ich weiß, was foreach ist, was es tut und wie es verwendet wird. Diese Frage betrifft die Funktionsweise unter der Motorhaube, und ich möchte keine Antworten im Sinne von "So schleifen Sie ein Array mit foreach".


Ich habe lange angenommen, dass foreach mit dem Array selbst funktioniert. Dann habe ich viele Hinweise darauf gefunden, dass es mit einer Kopie des Arrays funktioniert, und ich habe seitdem angenommen, dass dies das Ende der Geschichte ist. Aber ich bin kürzlich in eine Diskussion darüber geraten und habe nach einigem Experimentieren festgestellt, dass dies tatsächlich nicht zu 100% zutrifft.

Lass mich zeigen, was ich meine. Für die folgenden Testfälle arbeiten wir mit dem folgenden Array:

$array = array(1, 2, 3, 4, 5);

Testfall 1 :

foreach ($array as $item) {
  echo "$item\n";
  $array[] = $item;
}
print_r($array);

/* Output in loop:    1 2 3 4 5
   $array after loop: 1 2 3 4 5 1 2 3 4 5 */

Dies zeigt deutlich, dass wir nicht direkt mit dem Quell-Array arbeiten - andernfalls würde die Schleife für immer fortgesetzt, da wir während der Schleife ständig Elemente auf das Array verschieben. Aber nur um sicherzugehen, dass dies der Fall ist:

Testfall 2 :

foreach ($array as $key => $item) {
  $array[$key + 1] = $item + 2;
  echo "$item\n";
}

print_r($array);

/* Output in loop:    1 2 3 4 5
   $array after loop: 1 3 4 5 6 7 */

Dies bestätigt unsere anfängliche Schlussfolgerung, dass wir während der Schleife mit einer Kopie des Quell-Arrays arbeiten, da wir sonst die geänderten Werte während der Schleife sehen würden. Aber ...

Wenn wir in das Handbuch schauen, finden wir diese Aussage:

Wenn foreach zum ersten Mal ausgeführt wird, wird der interne Array-Zeiger automatisch auf das erste Element des Arrays zurückgesetzt.

Richtig ... dies scheint darauf hinzudeuten, dass foreach auf den Array-Zeiger des Quell-Arrays angewiesen ist. Aber wir haben gerade bewiesen, dass wir nicht mit dem Quell-Array arbeiten, oder? Nicht ganz.

Testfall :

// Move the array pointer on one to make sure it doesn't affect the loop
var_dump(each($array));

foreach ($array as $item) {
  echo "$item\n";
}

var_dump(each($array));

/* Output
  array(4) {
    [1]=>
    int(1)
    ["value"]=>
    int(1)
    [0]=>
    int(0)
    ["key"]=>
    int(0)
  }
  1
  2
  3
  4
  5
  bool(false)
*/

Obwohl wir nicht direkt mit dem Quell-Array arbeiten, arbeiten wir direkt mit dem Quell-Array-Zeiger - die Tatsache, dass sich der Zeiger am Ende des Arrays am Ende der Schleife befindet, zeigt dies. Es sei denn, dies kann nicht wahr sein - wenn dies der Fall wäre, würde Testfall 1 eine Endlosschleife ausführen.

Das PHP Handbuch besagt auch:

Da foreach darauf angewiesen ist, dass der interne Array-Zeiger innerhalb der Schleife geändert wird, kann dies zu unerwartetem Verhalten führen.

Nun, lassen Sie uns herausfinden, was dieses "unerwartete Verhalten" ist (technisch gesehen ist jedes Verhalten unerwartet, da ich nicht mehr weiß, was mich erwartet).

Testfall 4 :

foreach ($array as $key => $item) {
  echo "$item\n";
  each($array);
}

/* Output: 1 2 3 4 5 */

Testfall 5 :

foreach ($array as $key => $item) {
  echo "$item\n";
  reset($array);
}

/* Output: 1 2 3 4 5 */

... nichts Unerwartetes, in der Tat scheint es die "Kopie der Quelle" -Theorie zu unterstützen.


Die Frage

Was geht hier vor sich? Mein C-fu ist nicht gut genug für mich, um eine richtige Schlussfolgerung zu ziehen, indem ich einfach den PHP -Quellcode betrachte. Ich würde es begrüßen, wenn jemand es für mich ins Englische übersetzen könnte.

Es scheint mir, dass foreach mit einer Kopie des Arrays arbeitet, aber den Arrayzeiger des Quellarrays auf das Ende des Arrays nach der Schleife setzt.

  • Ist das richtig und die ganze Geschichte?
  • Wenn nicht, was macht es dann wirklich?
  • Gibt es eine Situation, in der die Verwendung von Funktionen, die den Array-Zeiger (each(), reset() usw.) während eines foreach anpassen, das Ergebnis der Schleife beeinflussen könnte?
1870
DaveRandom

foreach unterstützt die Iteration über drei verschiedene Arten von Werten:

Im Folgenden werde ich versuchen, genau zu erklären, wie die Iteration in verschiedenen Fällen funktioniert. Bei weitem der einfachste Fall sind Traversable -Objekte, da für diese foreach -Objekte im Wesentlichen nur die Syntax sugar für Code in dieser Richtung gilt:

foreach ($it as $k => $v) { /* ... */ }

/* translates to: */

if ($it instanceof IteratorAggregate) {
    $it = $it->getIterator();
}
for ($it->rewind(); $it->valid(); $it->next()) {
    $v = $it->current();
    $k = $it->key();
    /* ... */
}

Bei internen Klassen werden tatsächliche Methodenaufrufe vermieden, indem eine interne API verwendet wird, die im Wesentlichen nur die Schnittstelle Iterator auf C-Ebene widerspiegelt.

Die Iteration von Arrays und einfachen Objekten ist erheblich komplizierter. Zuallererst sollte beachtet werden, dass in PHP "Arrays" wirklich geordnete Wörterbücher sind und sie gemäß dieser Reihenfolge durchlaufen werden (die mit der Einfügereihenfolge übereinstimmt, solange Sie nicht etwas wie sort verwendet haben. ). Dies steht im Gegensatz zu einer Iteration durch die natürliche Reihenfolge der Schlüssel (wie Listen in anderen Sprachen häufig funktionieren) oder durch das Fehlen einer definierten Reihenfolge (wie Wörterbücher in anderen Sprachen häufig funktionieren).

Das Gleiche gilt auch für Objekte, da die Objekteigenschaften als ein anderes (geordnetes) Wörterbuch angesehen werden können, das die Eigenschaftsnamen ihren Werten zuordnet, sowie eine gewisse Sichtbarkeitsbehandlung. In den meisten Fällen werden die Objekteigenschaften nicht auf diese ineffiziente Weise gespeichert. Wenn Sie jedoch über ein Objekt iterieren, wird die normalerweise verwendete gepackte Darstellung in ein reales Wörterbuch konvertiert. An diesem Punkt wird die Iteration einfacher Objekte der Iteration von Arrays sehr ähnlich (weshalb ich hier nicht viel über die Iteration einfacher Objekte spreche).

So weit, ist es gut. Das Durchsuchen eines Wörterbuchs kann nicht zu schwierig sein, oder? Die Probleme beginnen, wenn Sie feststellen, dass sich ein Array/Objekt während der Iteration ändern kann. Dies kann auf verschiedene Arten geschehen:

  • Wenn Sie mit foreach ($arr as &$v) durch Referenz iterieren, wird $arr zu einer Referenz, die Sie während der Iteration ändern können.
  • In PHP 5 gilt dasselbe, auch wenn Sie nach Wert iterieren, aber das Array war zuvor eine Referenz: $ref =& $arr; foreach ($ref as $v)
  • Objekte haben eine Semantik für die Übergabe von By-Handles, was für die meisten praktischen Zwecke bedeutet, dass sie sich wie Referenzen verhalten. So können Objekte während der Iteration immer geändert werden.

Das Problem beim Zulassen von Änderungen während der Iteration besteht darin, dass das Element, auf dem Sie sich gerade befinden, entfernt wird. Angenommen, Sie verwenden einen Zeiger, um festzustellen, an welchem ​​Array-Element Sie sich gerade befinden. Wenn dieses Element jetzt freigegeben wird, bleibt ein baumelnder Zeiger übrig (was normalerweise zu einem Segfault führt).

Es gibt verschiedene Möglichkeiten, dieses Problem zu lösen. PHP 5 und PHP 7 unterscheiden sich in dieser Hinsicht erheblich und ich werde beide Verhaltensweisen im Folgenden beschreiben. Die Zusammenfassung ist, dass der Ansatz von PHP 5 ziemlich dumm war und zu allen möglichen seltsamen Randproblemen führte, während der umfassendere Ansatz von PHP 7 zu vorhersehbarerem und konsistenterem Verhalten führte.

Als letztes vorläufiges sollte beachtet werden, dass PHP Referenzzählung und Copy-on-Write verwendet, um den Speicher zu verwalten. Dies bedeutet, dass Sie, wenn Sie einen Wert "kopieren", tatsächlich nur den alten Wert wiederverwenden und seinen Referenzzähler erhöhen (refcount). Nur wenn Sie eine Änderung vornehmen, wird eine echte Kopie ("Vervielfältigung" genannt) erstellt. Siehe Sie werden angelogen für eine ausführlichere Einführung zu diesem Thema.

PHP 5

Interner Array-Zeiger und HashPointer

Arrays in PHP 5 verfügen über einen dedizierten "Internal Array Pointer" (IAP), der Änderungen ordnungsgemäß unterstützt: Wenn ein Element entfernt wird, wird überprüft, ob der IAP auf dieses Element verweist. Wenn dies der Fall ist, wird stattdessen zum nächsten Element weitergeschaltet.

Während foreach den IAP verwendet, gibt es eine zusätzliche Komplikation: Es gibt nur einen IAP, aber ein Array kann Teil mehrerer foreach-Schleifen sein:

// Using by-ref iteration here to make sure that it's really
// the same array in both loops and not a copy
foreach ($arr as &$v1) {
    foreach ($arr as &$v) {
        // ...
    }
}

Um zwei gleichzeitige Schleifen mit nur einem internen Array-Zeiger zu unterstützen, führt foreach die folgenden Aktionen aus: Bevor der Schleifenkörper ausgeführt wird, sichert foreach einen Zeiger auf das aktuelle Element und seinen Hash in einem foreach HashPointer. Nachdem der Schleifenkörper ausgeführt wurde, wird der IAP auf dieses Element zurückgesetzt, sofern es noch vorhanden ist. Wenn das Element jedoch entfernt wurde, verwenden wir es nur dort, wo sich der IAP gerade befindet. Dieses Schema funktioniert größtenteils irgendwie, aber es gibt eine Menge seltsamer Verhaltensweisen, die Sie daraus ziehen können. Einige davon werde ich im Folgenden demonstrieren.

Array-Vervielfältigung

Der IAP ist ein sichtbares Merkmal eines Arrays (das über die current -Familie verfügbar gemacht wird), da solche Änderungen am IAP als Änderungen in der Schreibkopie-Semantik gelten. Dies bedeutet leider, dass foreach in vielen Fällen gezwungen ist, das Array, über das iteriert wird, zu duplizieren. Die genauen Bedingungen sind:

  1. Das Array ist keine Referenz (is_ref = 0). Wenn es sich um einen Verweis handelt, sind Änderungen daranvermutetzu verbreiten, daher sollte er nicht dupliziert werden.
  2. Das Array hat einen Refcount> 1. Wenn refcount den Wert 1 hat, wird das Array nicht freigegeben und kann direkt geändert werden.

Wenn das Array nicht dupliziert ist (is_ref = 0, refcount = 1), wird nur sein refcount inkrementiert (*). Wenn außerdem foreach per Referenz verwendet wird, wird das (möglicherweise duplizierte) Array in eine Referenz umgewandelt.

Betrachten Sie diesen Code als Beispiel für das Auftreten von Duplikaten:

function iterate($arr) {
    foreach ($arr as $v) {}
}

$outerArr = [0, 1, 2, 3, 4];
iterate($outerArr);

Hier wird $arr dupliziert, um zu verhindern, dass IAP-Änderungen an $arr zu $outerArr gelangen. In Bezug auf die obigen Bedingungen ist das Array keine Referenz (is_ref = 0) und wird an zwei Stellen verwendet (refcount = 2). Diese Anforderung ist bedauerlich und ein Artefakt der suboptimalen Implementierung (es gibt hier keine Bedenken hinsichtlich einer Änderung während der Iteration, sodass wir den IAP nicht wirklich verwenden müssen).

(*) Das Inkrementieren von refcount klingt hier harmlos, verstößt jedoch gegen die COW-Semantik (Copy-on-Write) = 1 Werte. Diese Verletzung führt zu einer vom Benutzer sichtbaren Verhaltensänderung (während eine COW normalerweise transparent ist), da die IAP-Änderung auf dem iterierten Array beobachtet werden kann - jedoch nur bis zur ersten Nicht-IAP-Änderung auf dem Array. Stattdessen wären die drei "gültigen" Optionen a) immer zu duplizieren gewesen, b) würden das refcount nicht inkrementieren und somit ermöglichen, dass das iterierte Array in der Schleife willkürlich modifiziert wird, oder c) würden den IAP überhaupt nicht verwenden (the PHP 7 Lösung).

Reihenfolge der Positionserhöhung

Es gibt ein letztes Implementierungsdetail, das Sie beachten müssen, um die folgenden Codebeispiele richtig zu verstehen. Die "normale" Art, eine Datenstruktur zu durchlaufen, würde im Pseudocode ungefähr so ​​aussehen:

reset(arr);
while (get_current_data(arr, &data) == SUCCESS) {
    code();
    move_forward(arr);
}

Da es sich bei foreach jedoch um eine ziemlich spezielle Schneeflocke handelt, werden die Dinge etwas anders ausgeführt:

reset(arr);
while (get_current_data(arr, &data) == SUCCESS) {
    move_forward(arr);
    code();
}

Der Array-Zeiger ist nämlich bereits vorwärts bewegt, bevorder Schleifenkörper ausgeführt wird. Das heißt, während der Schleifenkörper am Element $i arbeitet, befindet sich der IAP bereits am Element $i+1. Dies ist der Grund, warum Codebeispiele, die Änderungen während der Iteration anzeigen, immer unset dasnext-Element und nicht das aktuelle.

Beispiele: Ihre Testfälle

Die drei oben beschriebenen Aspekte sollen Ihnen einen möglichst vollständigen Eindruck von den Besonderheiten der Implementierung von foreach vermitteln, und wir können einige Beispiele diskutieren.

Das Verhalten Ihrer Testfälle ist an dieser Stelle einfach zu erklären:

  • In den Testfällen 1 und 2 beginnt $array mit refcount = 1, daher wird es nicht durch foreach dupliziert: Nur das refcount wird inkrementiert. Wenn der Schleifenkörper anschließend das Array ändert (das an diesem Punkt den Wert refcount = 2 hat), erfolgt die Duplizierung an diesem Punkt. Foreach wird weiterhin an einer unveränderten Kopie von $array arbeiten.

  • In Testfall 3 wird das Array erneut nicht dupliziert, sodass foreach den IAP der Variablen $array ändert. Am Ende der Iteration ist der IAP NULL (was bedeutet, dass die Iteration durchgeführt wurde), was each durch die Rückgabe von false anzeigt.

  • In den Testfällen 4 und 5 sind sowohl each als auch reset Referenzfunktionen. Der $array hat einen refcount=2, wenn er an sie übergeben wird, daher muss er dupliziert werden. Als solches wird foreach wieder an einem separaten Array arbeiten.

Beispiele: Auswirkungen von current in foreach

Ein guter Weg, um die verschiedenen Duplizierungsverhaltensweisen zu veranschaulichen, besteht darin, das Verhalten der Funktion current() in einer Schleife foreach zu beobachten. Betrachten Sie dieses Beispiel:

foreach ($array as $val) {
    var_dump(current($array));
}
/* Output: 2 2 2 2 2 */

Hier sollten Sie wissen, dass current() eine by-ref-Funktion ist (eigentlich: prefer-ref), obwohl sie das Array nicht verändert. Es muss sein, um Nice mit all den anderen Funktionen wie next zu spielen, die alle by-ref sind. Das Übergeben von Verweisen impliziert, dass das Array getrennt werden muss und daher $array und foreach-array unterschiedlich sind. Der Grund, warum Sie 2 anstelle von 1 erhalten, ist ebenfalls oben erwähnt: foreach setzt den Array-Zeigervornach dem Ausführen des Benutzercodes, nicht nach. Obwohl sich der Code im ersten Element befindet, hat foreach den Zeiger bereits auf das zweite Element gesetzt.

Versuchen wir nun eine kleine Modifikation:

$ref = &$array;
foreach ($array as $val) {
    var_dump(current($array));
}
/* Output: 2 3 4 5 false */

Hier haben wir den Fall is_ref = 1, sodass das Array nicht kopiert wird (genau wie oben). Aber jetzt, da es sich um eine Referenz handelt, muss das Array nicht mehr dupliziert werden, wenn es an die Funktion by-ref current() übergeben wird. Daher arbeiten current() und foreach auf demselben Array. Aufgrund der Art und Weise, wie foreach den Zeiger vorschiebt, wird das Verhalten jedoch immer noch als "Off-by-One" angezeigt.

Sie erhalten das gleiche Verhalten, wenn Sie eine By-Ref-Iteration durchführen:

foreach ($array as &$val) {
    var_dump(current($array));
}
/* Output: 2 3 4 5 false */

Hier ist der wichtige Teil, dass foreach $array zu is_ref = 1 macht, wenn es durch Referenz iteriert wird, so dass Sie im Grunde die gleiche Situation wie oben haben.

Eine weitere kleine Variation, diesmal weisen wir das Array einer anderen Variablen zu:

$foo = $array;
foreach ($array as $val) {
    var_dump(current($array));
}
/* Output: 1 1 1 1 1 */

Hier ist der Refcount von $array 2, wenn die Schleife gestartet wird, also müssen wir ausnahmsweise das Duplizieren im Voraus durchführen. Somit sind $array und das von foreach verwendete Array von vornherein völlig getrennt. Aus diesem Grund erhalten Sie die Position des IAP an der Stelle, an der es sich vor der Schleife befand (in diesem Fall an der ersten Position).

Beispiele: Änderung während der Iteration

Bei dem Versuch, Änderungen während der Iteration zu berücksichtigen, sind alle unsere Foreach-Probleme aufgetreten. Daher werden hier einige Beispiele für diesen Fall betrachtet.

Betrachten Sie diese verschachtelten Schleifen über dasselbe Array (wobei die By-Ref-Iteration verwendet wird, um sicherzustellen, dass es wirklich dieselbe ist):

foreach ($array as &$v1) {
    foreach ($array as &$v2) {
        if ($v1 == 1 && $v2 == 1) {
            unset($array[1]);
        }
        echo "($v1, $v2)\n";
    }
}

// Output: (1, 1) (1, 3) (1, 4) (1, 5)

Der erwartete Teil hier ist, dass (1, 2) in der Ausgabe fehlt, weil das Element 1 entfernt wurde. Was wahrscheinlich unerwartet ist, ist, dass die äußere Schleife nach dem ersten Element stoppt. Warum ist das so?

Der Grund dafür ist der oben beschriebene Nested-Loop-Hack: Bevor der Loop-Body ausgeführt wird, werden die aktuelle IAP-Position und der Hash in einem HashPointer gesichert. Nach dem Schleifenkörper wird er wiederhergestellt, aber nur, wenn das Element noch vorhanden ist. Andernfalls wird stattdessen die aktuelle IAP-Position (wie auch immer sie sein mag) verwendet. Im obigen Beispiel ist dies genau der Fall: Das aktuelle Element der äußeren Schleife wurde entfernt, sodass der IAP verwendet wird, der bereits von der inneren Schleife als beendet markiert wurde!

Eine weitere Konsequenz des Sicherungs- und Wiederherstellungsmechanismus von HashPointer ist, dass Änderungen am IAP, obwohl reset() usw. in der Regel keine Auswirkungen auf foreach haben. Der folgende Code wird beispielsweise ausgeführt, als ob die reset() überhaupt nicht vorhanden wäre:

$array = [1, 2, 3, 4, 5];
foreach ($array as &$value) {
    var_dump($value);
    reset($array);
}
// output: 1, 2, 3, 4, 5

Der Grund dafür ist, dass reset() den IAP zwar vorübergehend ändert, aber nach dem Schleifenkörper im aktuellen foreach-Element wiederhergestellt wird. Um zu erzwingen, dass reset() eine Auswirkung auf die Schleife hat, müssen Sie zusätzlich das aktuelle Element entfernen, damit der Sicherungs-/Wiederherstellungsmechanismus fehlschlägt:

$array = [1, 2, 3, 4, 5];
$ref =& $array;
foreach ($array as $value) {
    var_dump($value);
    unset($array[1]);
    reset($array);
}
// output: 1, 1, 3, 4, 5

Aber diese Beispiele sind immer noch vernünftig. Der eigentliche Spaß beginnt, wenn Sie sich daran erinnern, dass bei der Wiederherstellung von HashPointer ein Zeiger auf das Element und dessen Hash verwendet wird, um festzustellen, ob es noch vorhanden ist. Aber: Hashes haben Kollisionen und Zeiger können wiederverwendet werden! Dies bedeutet, dass wir mit einer sorgfältigen Auswahl der Array-Schlüssel foreach glauben lassen können, dass ein entferntes Element noch vorhanden ist, sodass es direkt dorthin springt. Ein Beispiel:

$array = ['EzEz' => 1, 'EzFY' => 2, 'FYEz' => 3];
$ref =& $array;
foreach ($array as $value) {
    unset($array['EzFY']);
    $array['FYFY'] = 4;
    reset($array);
    var_dump($value);
}
// output: 1, 4

Hier sollten wir normalerweise die Ausgabe 1, 1, 3, 4 gemäß den vorherigen Regeln erwarten. Was passiert, ist, dass 'FYFY' denselben Hash wie das entfernte Element 'EzFY' hat und der Allokator zufällig denselben Speicherort zum Speichern des Elements wiederverwendet. So springt foreach direkt auf das neu eingefügte Element und schneidet so die Schleife ab.

Ersetzen der iterierten Entität während der Schleife

Ein letzter seltsamer Fall, den ich erwähnen möchte, ist, dass PHP es Ihnen ermöglicht, die iterierte Entität während der Schleife zu ersetzen. Sie können also ein Array iterieren und es dann zur Hälfte durch ein anderes Array ersetzen. Oder beginnen Sie mit der Iteration in einem Array und ersetzen Sie es durch ein Objekt:

$arr = [1, 2, 3, 4, 5];
$obj = (object) [6, 7, 8, 9, 10];

$ref =& $arr;
foreach ($ref as $val) {
    echo "$val\n";
    if ($val == 3) {
        $ref = $obj;
    }
}
/* Output: 1 2 3 6 7 8 9 10 */

Wie Sie in diesem Fall sehen können, startet PHP die Iteration der anderen Entität von Anfang an, sobald die Substitution stattgefunden hat.

PHP 7

Hashtable-Iteratoren

Wenn Sie sich noch erinnern, bestand das Hauptproblem bei der Array-Iteration darin, wie das Entfernen von Elementen während der Iteration gehandhabt wird. PHP 5 verwendete zu diesem Zweck einen einzelnen internen Array-Zeiger (IAP), der etwas suboptimal war, da ein Array-Zeiger gestreckt werden musste, um mehrere foreach-Schleifen gleichzeitig zu unterstützenundInteraktion mit reset() usw. obendrein.

PHP 7 verwendet einen anderen Ansatz, nämlich die Erstellung einer beliebigen Anzahl externer, sicherer Hashtable-Iteratoren. Diese Iteratoren müssen im Array registriert werden. Ab diesem Zeitpunkt haben sie dieselbe Semantik wie der IAP: Wenn ein Array-Element entfernt wird, werden alle auf dieses Element zeigenden Hashtable-Iteratoren zum nächsten Element weitergeschaltet.

Dies bedeutet, dass foreach den IAP überhaupt nicht mehr verwendet . Die foreach-Schleife hat keinerlei Einfluss auf die Ergebnisse von current() usw. und ihr eigenes Verhalten wird niemals durch Funktionen wie reset() usw. beeinflusst.

Array-Vervielfältigung

Eine weitere wichtige Änderung zwischen PHP 5 und PHP 7 betrifft die Duplizierung von Arrays. Jetzt, da der IAP nicht mehr verwendet wird, führt die By-Value-Array-Iteration in allen Fällen nur noch ein Inkrement von refcount aus (anstatt das Array zu duplizieren). Wenn das Array während der foreach-Schleife geändert wird, tritt an diesem Punkt eine Duplizierung auf (laut Copy-on-Write), und foreach arbeitet auf dem alten Array weiter.

In den meisten Fällen ist diese Änderung transparent und hat keine andere Auswirkung als eine bessere Leistung. Es gibt jedoch eine Gelegenheit, die zu einem anderen Verhalten führt, nämlich den Fall, in dem das Array zuvor eine Referenz war:

$array = [1, 2, 3, 4, 5];
$ref = &$array;
foreach ($array as $val) {
    var_dump($val);
    $array[2] = 0;
}
/* Old output: 1, 2, 0, 4, 5 */
/* New output: 1, 2, 3, 4, 5 */

Bisher waren By-Value-Iterationen von Referenz-Arrays Sonderfälle. In diesem Fall trat keine Duplizierung auf, sodass alle Änderungen des Arrays während der Iteration in der Schleife wiedergegeben werden. In PHP 7 ist dieser Sonderfall behoben: Bei einer Iteration eines Arrays nach Werten wird immer an den ursprünglichen Elementen gearbeitet. Änderungen während der Schleife werden nicht berücksichtigt.

Dies gilt natürlich nicht für die Iteration nach Verweis. Wenn Sie per Referenz iterieren, werden alle Änderungen in der Schleife wiedergegeben. Interessanterweise gilt das Gleiche für die By-Value-Iteration einfacher Objekte:

$obj = new stdClass;
$obj->foo = 1;
$obj->bar = 2;
foreach ($obj as $val) {
    var_dump($val);
    $obj->bar = 42;
}
/* Old and new output: 1, 42 */

Dies spiegelt die By-Handle-Semantik von Objekten wider (d. H. Sie verhalten sich selbst in By-Value-Kontexten referenzartig).

Beispiele

Betrachten wir einige Beispiele, beginnend mit Ihren Testfällen:

  • Die Testfälle 1 und 2 behalten die gleiche Ausgabe bei: Die By-Value-Array-Iteration arbeitet immer an den ursprünglichen Elementen. (In diesem Fall sind sogar refcounting und das Duplizierungsverhalten zwischen PHP 5 und PHP 7 genau gleich.).

  • Änderungen in Testfall 3: Foreach verwendet den IAP nicht mehr, sodass each() von der Schleife nicht betroffen ist. Es wird davor und danach die gleiche Ausgabe haben.

  • Die Testfälle 4 und 5 bleiben unverändert: each() und reset() duplizieren das Array vor dem Ändern des IAP, während foreach weiterhin das ursprüngliche Array verwendet. (Nicht, dass die IAP-Änderung von Bedeutung gewesen wäre, selbst wenn das Array gemeinsam genutzt worden wäre.)

Die zweite Gruppe von Beispielen bezog sich auf das Verhalten von current() in verschiedenen reference/refcounting Konfigurationen. Dies ist nicht mehr sinnvoll, da current() von der Schleife nicht mehr betroffen ist und der Rückgabewert immer gleich bleibt.

Es gibt jedoch einige interessante Änderungen, wenn Änderungen während der Iteration berücksichtigt werden. Ich hoffe, Sie werden das neue Verhalten vernünftiger finden. Das erste Beispiel:

$array = [1, 2, 3, 4, 5];
foreach ($array as &$v1) {
    foreach ($array as &$v2) {
        if ($v1 == 1 && $v2 == 1) {
            unset($array[1]);
        }
        echo "($v1, $v2)\n";
    }
}

// Old output: (1, 1) (1, 3) (1, 4) (1, 5)
// New output: (1, 1) (1, 3) (1, 4) (1, 5)
//             (3, 1) (3, 3) (3, 4) (3, 5)
//             (4, 1) (4, 3) (4, 4) (4, 5)
//             (5, 1) (5, 3) (5, 4) (5, 5) 

Wie Sie sehen, bricht die äußere Schleife nach der ersten Iteration nicht mehr ab. Der Grund dafür ist, dass beide Schleifen nun vollständig separate Hashtable-Iteratoren haben und keine Kreuzkontamination beider Schleifen durch einen gemeinsam genutzten IAP mehr besteht.

Ein weiterer seltsamer Edge-Fall, der jetzt behoben ist, ist der seltsame Effekt, den Sie erhalten, wenn Sie Elemente entfernen und hinzufügen, die zufällig denselben Hash haben:

$array = ['EzEz' => 1, 'EzFY' => 2, 'FYEz' => 3];
foreach ($array as &$value) {
    unset($array['EzFY']);
    $array['FYFY'] = 4;
    var_dump($value);
}
// Old output: 1, 4
// New output: 1, 3, 4

Zuvor sprang der HashPointer-Wiederherstellungsmechanismus direkt zum neuen Element, da er aussah, als wäre er derselbe wie das entfernte Element (aufgrund einer Kollision von Hash und Zeiger). Da wir uns für nichts mehr auf das Element Hash verlassen, ist dies kein Problem mehr.

1551
NikiC

In Beispiel 3 ändern Sie das Array nicht. In allen anderen Beispielen ändern Sie entweder den Inhalt oder den internen Array-Zeiger. Dies ist aufgrund der Semantik des Zuweisungsoperators wichtig, wenn es um PHP - Arrays geht.

Der Zuweisungsoperator für die Arrays in PHP funktioniert eher wie ein Lazy Clone. Wenn Sie eine Variable einer anderen zuweisen, die ein Array enthält, wird das Array im Gegensatz zu den meisten Sprachen geklont. Das eigentliche Klonen wird jedoch nur durchgeführt, wenn es erforderlich ist. Dies bedeutet, dass der Klon nur stattfindet, wenn eine der Variablen geändert wird (Copy-on-Write).

Hier ist ein Beispiel:

$a = array(1,2,3);
$b = $a;  // This is lazy cloning of $a. For the time
          // being $a and $b point to the same internal
          // data structure.

$a[] = 3; // Here $a changes, which triggers the actual
          // cloning. From now on, $a and $b are two
          // different data structures. The same would
          // happen if there were a change in $b.

Wenn Sie zu Ihren Testfällen zurückkehren, können Sie sich leicht vorstellen, dass foreach eine Art Iterator mit einem Verweis auf das Array erstellt. Diese Referenz funktioniert genauso wie die Variable $b in meinem Beispiel. Der Iterator und die Referenz sind jedoch nur während der Schleife aktiv und werden dann beide verworfen. Jetzt können Sie sehen, dass in allen Fällen außer 3 das Array während der Schleife geändert wird, während diese zusätzliche Referenz aktiv ist. Dies löst einen Klon aus und das erklärt, was hier los ist!

Hier ist ein ausgezeichneter Artikel für einen weiteren Nebeneffekt dieses Verhaltens beim Kopieren beim Schreiben: Der PHP Ternäre Operator: Schnell oder nicht?

109
linepogl

Einige Punkte, die bei der Arbeit mit foreach() zu beachten sind:

a) foreach bearbeitet die voraussichtliche Kopie des ursprünglichen Arrays. Dies bedeutet, dass foreach() den Datenspeicher GETEILT hat, bis oder solange kein prospected copy erstellt wird für alle Notizen/Benutzerkommentare .

b) Was löst eine voraussichtliche Kopie aus? Eine potenzielle Kopie wird basierend auf der Richtlinie von copy-on-write erstellt, dh, wenn ein an foreach() übergebenes Array geändert wird, wird ein Klon des ursprünglichen Arrays erstellt.

c) Das ursprüngliche Array und der Iterator foreach() haben DISTINCT SENTINEL VARIABLES, dh einen für das ursprüngliche Array und einen für foreach; siehe den Testcode unten. SPL , Iteratoren und Array-Iterator .

Stack Overflow question Wie wird sichergestellt, dass der Wert in einer 'foreach'-Schleife in PHP zurückgesetzt wird? behandelt die Fälle (3,4,5) von Ihre Frage.

Das folgende Beispiel zeigt, dass sich jedes () und jedes reset () NICHT auf SENTINEL Variablen (for example, the current index variable) des foreach() Iterators auswirkt.

$array = array(1, 2, 3, 4, 5);

list($key2, $val2) = each($array);
echo "each() Original (outside): $key2 => $val2<br/>";

foreach($array as $key => $val){
    echo "foreach: $key => $val<br/>";

    list($key2,$val2) = each($array);
    echo "each() Original(inside): $key2 => $val2<br/>";

    echo "--------Iteration--------<br/>";
    if ($key == 3){
        echo "Resetting original array pointer<br/>";
        reset($array);
    }
}

list($key2, $val2) = each($array);
echo "each() Original (outside): $key2 => $val2<br/>";

Ausgabe:

each() Original (outside): 0 => 1
foreach: 0 => 1
each() Original(inside): 1 => 2
--------Iteration--------
foreach: 1 => 2
each() Original(inside): 2 => 3
--------Iteration--------
foreach: 2 => 3
each() Original(inside): 3 => 4
--------Iteration--------
foreach: 3 => 4
each() Original(inside): 4 => 5
--------Iteration--------
Resetting original array pointer
foreach: 4 => 5
each() Original(inside): 0=>1
--------Iteration--------
each() Original (outside): 1 => 2
43
sakhunzai

HINWEIS FÜR PHP 7

Um diese Antwort zu aktualisieren, da sie an Popularität gewonnen hat: Diese Antwort gilt nicht mehr ab PHP 7. Wie im Abschnitt " Rückwärts inkompatible Änderungen " in PHP 7 foreach bearbeitet die Kopie des Arrays, sodass Änderungen am Array selbst nicht in der foreach-Schleife berücksichtigt werden. Weitere Details unter dem Link.

Erklärung (Zitat aus php.net ):

Das erste Formular durchläuft das durch array_expression angegebene Array. Bei jeder Iteration wird der Wert des aktuellen Elements $ value zugewiesen und der interne Array-Zeiger um eins vorgerückt (bei der nächsten Iteration wird also das nächste Element angezeigt).

In Ihrem ersten Beispiel haben Sie also nur ein Element im Array, und wenn der Zeiger bewegt wird, existiert das nächste Element nicht. Nachdem Sie also ein neues Element für jedes Ende hinzugefügt haben, hat es bereits entschieden, dass es das letzte Element ist.

In Ihrem zweiten Beispiel beginnen Sie mit zwei Elementen, und foreach loop befindet sich nicht am letzten Element, sodass es das Array bei der nächsten Iteration auswertet und somit erkennt, dass das Array ein neues Element enthält.

Ich glaube, dass dies alles eine Konsequenz aus jedem Iterationsteil der Erklärung in der Dokumentation ist, was wahrscheinlich bedeutet, dass foreach zuvor die gesamte Logik ausführt es ruft den Code in {} auf.

Testfall

Wenn Sie dies ausführen:

<?
    $array = Array(
        'foo' => 1,
        'bar' => 2
    );
    foreach($array as $k=>&$v) {
        $array['baz']=3;
        echo $v." ";
    }
    print_r($array);
?>

Sie erhalten diese Ausgabe:

1 2 3 Array
(
    [foo] => 1
    [bar] => 2
    [baz] => 3
)

Das bedeutet, dass es die Änderung akzeptiert und durchlaufen hat, weil sie "rechtzeitig" geändert wurde. Aber wenn Sie dies tun:

<?
    $array = Array(
        'foo' => 1,
        'bar' => 2
    );
    foreach($array as $k=>&$v) {
        if ($k=='bar') {
            $array['baz']=3;
        }
        echo $v." ";
    }
    print_r($array);
?>

Sie erhalten:

1 2 Array
(
    [foo] => 1
    [bar] => 2
    [baz] => 3
)

Das bedeutet, dass das Array geändert wurde, aber da wir es geändert haben, als sich foreach bereits am letzten Element des Arrays befand, "entschied" es sich, keine Schleife mehr auszuführen, und obwohl wir ein neues Element hinzugefügt haben, haben wir es hinzugefügt " zu spät "und es wurde nicht durchgeschleift.

Eine ausführliche Erklärung finden Sie unter Wie funktioniert PHP 'foreach' tatsächlich? , wodurch die Interna hinter diesem Verhalten erklärt werden.

29
Damir Kasipovic

Gemäß der Dokumentation im PHP Handbuch.

Bei jeder Iteration wird der Wert des aktuellen Elements $ v und dem internen Element zugewiesen
Der Array-Zeiger wird um eins vorgerückt (bei der nächsten Iteration schauen Sie sich also das nächste Element an).

Also nach deinem ersten Beispiel:

$array = ['foo'=>1];
foreach($array as $k=>&$v)
{
   $array['bar']=2;
   echo($v);
}

$array hat nur ein einzelnes Element. Entsprechend der foreach-Ausführung 1 wird $v zugewiesen, und es gibt kein anderes Element, um den Zeiger zu bewegen

Aber in Ihrem zweiten Beispiel:

$array = ['foo'=>1, 'bar'=>2];
foreach($array as $k=>&$v)
{
   $array['baz']=3;
   echo($v);
}

$array haben zwei Elemente, also wertet $ array jetzt die Nullindizes aus und verschiebt den Zeiger um eins. Fügen Sie für die erste Iteration der Schleife $array['baz']=3; als Referenzübergabe hinzu.

14
user3535130

Gute Frage, da viele Entwickler, auch erfahrene, durch die Art und Weise, wie PHP Arrays in foreach-Schleifen behandelt, verwirrt sind. In der Standard-foreach-Schleife erstellt PHP eine Kopie des Arrays, das in der Schleife verwendet wird. Die Kopie wird sofort nach Beendigung der Schleife gelöscht. Dies ist beim Betrieb einer einfachen foreach-Schleife transparent. Zum Beispiel:

$set = array("Apple", "banana", "coconut");
foreach ( $set AS $item ) {
    echo "{$item}\n";
}

Dies gibt aus:

Apple
banana
coconut

Die Kopie wird erstellt, aber der Entwickler merkt es nicht, da auf das ursprüngliche Array in der Schleife oder nach Beendigung der Schleife nicht verwiesen wird. Wenn Sie jedoch versuchen, die Elemente in einer Schleife zu ändern, stellen Sie fest, dass diese unverändert sind, wenn Sie fertig sind:

$set = array("Apple", "banana", "coconut");
foreach ( $set AS $item ) {
    $item = strrev ($item);
}

print_r($set);

Dies gibt aus:

Array
(
    [0] => Apple
    [1] => banana
    [2] => coconut
)

Änderungen gegenüber dem Original können nicht bemerkt werden, es gibt tatsächlich keine Änderungen gegenüber dem Original, obwohl Sie $ item eindeutig einen Wert zugewiesen haben. Dies liegt daran, dass Sie mit $ item arbeiten, wie es in der Kopie von $ set erscheint, an der gearbeitet wird. Sie können dies außer Kraft setzen, indem Sie $ item nach Verweis greifen, wie folgt:

$set = array("Apple", "banana", "coconut");
foreach ( $set AS &$item ) {
    $item = strrev($item);
}
print_r($set);

Dies gibt aus:

Array
(
    [0] => elppa
    [1] => ananab
    [2] => tunococ
)

Es ist also offensichtlich und beobachtbar, dass die an $ item vorgenommenen Änderungen an den Mitgliedern des ursprünglichen $ set vorgenommen werden, wenn $ item anhand von Verweisen bearbeitet wird. Die Verwendung von $ item als Referenz verhindert auch, dass PHP die Array-Kopie erstellt. Um dies zu testen, zeigen wir zuerst ein schnelles Skript, das die Kopie demonstriert:

$set = array("Apple", "banana", "coconut");
foreach ( $set AS $item ) {
    $set[] = ucfirst($item);
}
print_r($set);

Dies gibt aus:

Array
(
    [0] => Apple
    [1] => banana
    [2] => coconut
    [3] => Apple
    [4] => Banana
    [5] => Coconut
)

Wie im Beispiel gezeigt, hat PHP $ set kopiert und zum Durchlaufen verwendet, aber als $ set in der Schleife verwendet wurde, hat PHP die Variablen zum ursprünglichen Array hinzugefügt. nicht das kopierte Array. Grundsätzlich verwendet PHP nur das kopierte Array für die Ausführung der Schleife und die Zuweisung von $ item. Aus diesem Grund wird die obige Schleife nur dreimal ausgeführt, und jedes Mal wird ein anderer Wert an das Ende der ursprünglichen $ Menge angehängt, wobei die ursprüngliche $ Menge mit 6 Elementen belassen wird, jedoch niemals eine Endlosschleife betreten wird.

Was wäre jedoch, wenn wir, wie bereits erwähnt, $ item als Referenz verwendet hätten? Ein einzelnes Zeichen zum obigen Test hinzugefügt:

$set = array("Apple", "banana", "coconut");
foreach ( $set AS &$item ) {
    $set[] = ucfirst($item);
}
print_r($set);

Ergibt eine Endlosschleife. Beachten Sie, dass dies tatsächlich eine Endlosschleife ist. Sie müssen entweder das Skript selbst beenden oder warten, bis Ihr Betriebssystem nicht mehr genügend Arbeitsspeicher hat. Ich habe meinem Skript die folgende Zeile hinzugefügt, damit PHP sehr schnell keinen Speicher mehr hat. Wenn Sie diese Endlosschleifentests ausführen, sollten Sie dasselbe tun:

ini_set("memory_limit","1M");

In diesem vorherigen Beispiel mit der Endlosschleife sehen wir den Grund, warum PHP geschrieben wurde, um eine Kopie des Arrays zu erstellen, über das eine Schleife erstellt werden soll. Wenn eine Kopie erstellt und nur von der Struktur des Schleifenkonstrukts selbst verwendet wird, bleibt das Array während der Ausführung der Schleife statisch, sodass Sie niemals auf Probleme stoßen.

11
hrvojeA

PHP foreach loop kann mit Indexed arrays, Associative arrays und Object public variables verwendet werden.

In foreach loop erzeugt php als erstes eine Kopie des Arrays, über das iteriert werden soll. PHP durchläuft dann dieses neue copy des Arrays und nicht das ursprüngliche. Dies wird im folgenden Beispiel gezeigt:

<?php
$numbers = [1,2,3,4,5,6,7,8,9]; # initial values for our array
echo '<pre>', print_r($numbers, true), '</pre>', '<hr />';
foreach($numbers as $index => $number){
    $numbers[$index] = $number + 1; # this is making changes to the origial array
    echo 'Inside of the array = ', $index, ': ', $number, '<br />'; # showing data from the copied array
}
echo '<hr />', '<pre>', print_r($numbers, true), '</pre>'; # shows the original values (also includes the newly added values).

Außerdem erlaubt PHP die Verwendung von iterated values as a reference to the original array value. Dies wird nachfolgend demonstriert:

<?php
$numbers = [1,2,3,4,5,6,7,8,9];
echo '<pre>', print_r($numbers, true), '</pre>';
foreach($numbers as $index => &$number){
    ++$number; # we are incrementing the original value
    echo 'Inside of the array = ', $index, ': ', $number, '<br />'; # this is showing the original value
}
echo '<hr />';
echo '<pre>', print_r($numbers, true), '</pre>'; # we are again showing the original value

Hinweis: original array indexes kann nicht als references verwendet werden.

Quelle: http://dwellupper.io/post/47/understanding-php-foreach-loop-with-examples

7
Pranav Rana