Frage Wie funktioniert PHP 'foreach' eigentlich?


Lassen Sie mich das voraussetzen, indem ich sage, dass ich weiß was foreach ist, tut und wie man es benutzt. Diese Frage betrifft, wie es unter der Motorhaube funktioniert, und ich möchte keine Antworten auf die Art von "wie Sie ein Array mit foreach".


Das habe ich lange angenommen foreach arbeitete mit dem Array selbst. Dann habe ich viele Hinweise darauf gefunden, dass es mit einem funktioniert Kopieren von der Anordnung, und ich habe seitdem angenommen, dass dies das Ende der Geschichte ist. Aber ich habe kürzlich eine Diskussion darüber geführt und nach ein paar Experimenten festgestellt, dass dies nicht 100% ig wahr ist.

Lass mich zeigen, was ich meine. Für die folgenden Testfälle arbeiten wir mit folgendem 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 - sonst würde die Schleife für immer fortfahren, da wir während des Loops ständig Elemente auf das Array schieben. Aber um sicher zu sein, ist dies der Fall:

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 sichert unsere erste Schlussfolgerung, wir arbeiten mit einer Kopie des Quell-Arrays während der Schleife, ansonsten würden wir die modifizierten Werte während der Schleife sehen. Aber...

Wenn wir in die Handbuch, finden wir diese Aussage:

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

Richtig ... das scheint darauf hinzudeuten foreach beruht auf dem Array-Zeiger des Quell-Arrays. Aber wir haben gerade bewiesen, dass wir es sind arbeitet nicht mit dem Quell-Array, Recht? Nun, nicht ganz.

Testfall 3:

// 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)
*/

Trotz der Tatsache, dass wir nicht direkt mit dem Quell-Array arbeiten, arbeiten wir direkt mit dem Quell-Array-Zeiger - die Tatsache, dass der Zeiger am Ende des Arrays am Ende der Schleife ist, zeigt dies. Außer das kann nicht wahr sein - wenn es war, dann Testfall 1 würde für immer wiederholen.

Das PHP-Handbuch besagt auch:

Da foreach auf den internen Array-Zeiger angewiesen ist, kann das Ändern innerhalb der Schleife zu unerwartetem Verhalten führen.

Nun, lasst uns herausfinden, was dieses "unerwartete Verhalten" ist (technisch ist jedes Verhalten unerwartet, da ich nicht mehr weiß, was ich zu erwarten habe).

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, tatsächlich scheint es die Theorie der "Kopie der Quelle" zu stü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 anschaue, ich würde mich freuen, wenn jemand es für mich ins Englische übersetzen könnte.

Es scheint mir, dass foreach arbeitet mit einem Kopieren des Arrays, sondern setzt den Array-Zeiger des Quell-Arrays nach der Schleife auf das Ende des Arrays.

  • Ist das richtig und die ganze Geschichte?
  • Wenn nicht, was macht es wirklich?
  • Gibt es Situationen, in denen Funktionen verwendet werden, die den Array-Zeiger anpassen (each(), reset() et al.) während einer foreach könnte das Ergebnis der Schleife beeinflussen?

1637
2018-04-07 19:33


Ursprung


Antworten:


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

Im folgenden werde ich versuchen, genau zu erläutern, wie die Iteration in den verschiedenen Fällen funktioniert. Bei weitem der einfachste Fall ist Traversable Objekte, wie für diese foreach ist im Wesentlichen nur Syntax Zucker für Code in diesen Zeilen:

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 ein internes API verwendet wird, das im Wesentlichen nur die Spiegelung des Iterator Schnittstelle auf der C-Ebene.

Die Iteration von Arrays und einfachen Objekten ist wesentlich komplizierter. Zuallererst ist zu beachten, dass "Arrays" in PHP wirklich geordnete Wörterbücher sind und dass sie entsprechend dieser Reihenfolge durchlaufen werden (was der Reihenfolge entspricht, so lange Sie nicht wie sort). Dies steht im Gegensatz zur Iteration durch die natürliche Reihenfolge der Schlüssel (wie funktionieren Listen in anderen Sprachen oft) oder ohne eine definierte Reihenfolge (wie Wörterbücher in anderen Sprachen oft funktionieren).

Das Gleiche gilt auch für Objekte, da die Objekteigenschaften als ein anderes (geordnetes) Dictionary betrachtet werden können, das Eigenschaftsnamen ihren Werten zuordnet, sowie eine gewisse Sichtbarkeitsverarbeitung. In der Mehrzahl der Fälle werden die Objekteigenschaften auf diese eher ineffiziente Weise nicht wirklich gespeichert. Wenn Sie jedoch über ein Objekt iterieren, wird die normalerweise verwendete gepackte Darstellung in ein echtes Wörterbuch konvertiert. An diesem Punkt wird die Iteration von einfachen Objekten der Iteration von Arrays sehr ähnlich (weshalb ich hier nicht viel über die Iteration einfacher Objekte diskutiere).

So weit, ist es gut. Iterieren über ein Wörterbuch kann nicht zu schwer sein, oder? Die Probleme beginnen, wenn Sie feststellen, dass sich ein Array / Objekt während der Iteration ändern kann. Es gibt mehrere Möglichkeiten, wie dies passieren kann:

  • Wenn Sie mit Verweis iterieren, verwenden Sie foreach ($arr as &$v) dann $arr wird in eine Referenz umgewandelt und Sie können sie während der Iteration ändern.
  • In PHP 5 gilt das Gleiche, auch wenn Sie nach Wert iterieren, aber das Array war vorher eine Referenz: $ref =& $arr; foreach ($ref as $v)
  • Objekte haben By-Handle Semantik, die für praktische 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 ist der Fall, bei dem das Element, auf dem Sie sich gerade befinden, entfernt wird. Angenommen, Sie verwenden einen Zeiger, um zu verfolgen, auf welchem ​​Array-Element Sie sich gerade befinden. Wenn dieses Element jetzt freigegeben wird, verbleibt ein ungeordneter Zeiger (der 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 Arten von seltsamen Randfallproblemen führte, während der komplexere Ansatz von PHP 7 zu besser vorhersagbarem und konsistentem Verhalten führt.

Als letzte Vorbemerkung sollte angemerkt werden, dass PHP Referenzzählung und Copy-on-Write zur Speicherverwaltung verwendet. Das heißt, wenn Sie einen Wert "kopieren", verwenden Sie einfach nur den alten Wert und erhöhen den Referenzzähler (refcount). Nur wenn Sie irgendeine Art von Modifikation durchführen, wird eine echte Kopie (eine "Duplizierung") durchgeführt. Sehen Du wirst belogen für eine ausführlichere Einführung zu diesem Thema.

PHP 5

Interner Array-Zeiger und HashPointer

Arrays in PHP 5 haben einen dedizierten "internen Array-Pointer" (IAP), der Modifikationen richtig unterstützt: Wann immer ein Element entfernt wird, wird geprüft, ob der IAP auf dieses Element zeigt. Wenn dies der Fall ist, wird es stattdessen zum nächsten Element weitergeleitet.

Während foreach den IAP nutzt, 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 simultane Schleifen mit nur einem internen Array-Zeiger zu unterstützen, führt foreach die folgenden Schenigans durch: Bevor der Schleifenkörper ausgeführt wird, sichert foreach einen Zeiger auf das aktuelle Element und seinen Hash in eine pro-foreach HashPointer. Nachdem der Schleifenkörper ausgeführt wurde, wird der IAP auf dieses Element zurückgesetzt, wenn es noch vorhanden ist. Wenn das Element jedoch entfernt wurde, verwenden wir einfach überall dort, wo sich das IAP gerade befindet. Dieses Schema funktioniert meistens irgendwie, aber es gibt eine Menge seltsames Verhalten, das man daraus ziehen kann, von denen ich einige unten demonstriere.

Array-Duplizierung

Das IAP ist ein sichtbares Merkmal eines Arrays (ausgesetzt durch die current Familie von Funktionen), so zählen Änderungen an der IAP als Modifikationen unter der Copy-on-Write-Semantik. Dies bedeutet leider, dass foreach in vielen Fällen gezwungen ist, das Array, über das es iteriert, zu duplizieren. Die genauen Bedingungen sind:

  1. Das Array ist keine Referenz (is_ref = 0). Wenn es eine Referenz ist, dann sind Änderungen daran soll propagieren, also sollte es nicht dupliziert werden.
  2. Das Array hat refcount> 1. Wenn refcount 1 ist, wird das Array nicht freigegeben und wir können es direkt ändern.

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

Betrachten Sie diesen Code als Beispiel für eine Duplizierung:

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

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

Hier, $arr wird dupliziert, um IAP-Änderungen zu verhindern $arr aus zu lecken $outerArr. 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 keine Bedenken bezüglich der Modifikation während der Iteration hier, so dass wir das IAP nicht wirklich verwenden müssen).

(*) Die Refcount hier zu inkrementieren klingt harmlos, verletzt aber die Copy-on-Write (COW) -Semantik: Das bedeutet, dass wir den IAP eines refcount = 2-Arrays modifizieren, während COW vorschreibt, dass Modifikationen nur auf refcount durchgeführt werden können = 1 Werte. Diese Verletzung führt zu einer vom Benutzer sichtbaren Verhaltensänderung (während COW normalerweise transparent ist), da die IAP-Änderung auf dem iterierten Array beobachtbar ist - aber nur bis zur ersten Nicht-IAP-Änderung auf dem Array. Stattdessen wären die drei "gültigen" Optionen a) immer zu duplizieren, b) nicht die Refcount zu inkrementieren und somit zu erlauben, dass das iterierte Array in der Schleife beliebig modifiziert wird, oder c) den IAP überhaupt nicht zu verwenden ( die 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 des Durchschleifens einiger Datenstrukturen würde im Pseudocode etwa so aussehen:

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

jedoch foreachDa es sich um eine ganz besondere Schneeflocke handelt, möchte man die Dinge etwas anders machen:

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

Der Array-Zeiger wird nämlich bereits vorwärts bewegt Vor der Schleifenkörper läuft. Dies bedeutet, dass der Schleifenkörper am Element arbeitet $i, das IAP ist schon an element $i+1. Dies ist der Grund, warum Codebeispiele, die während der Iteration eine Änderung zeigen, immer die Nächster Element, anstatt die aktuelle.

Beispiele: Ihre Testfälle

Die drei oben beschriebenen Aspekte sollten Ihnen einen weitgehend vollständigen Eindruck der Eigenheiten der foreach-Implementierung vermitteln und wir können uns dann einigen Beispielen zuwenden.

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

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

  • Im Testfall 3 wird das Array erneut nicht dupliziert, daher wird foreach den IAP des $array Variable. Am Ende der Iteration ist der IAP NULL (was Iteration bedeutet), was each zeigt durch zurück false.

  • In Testfällen 4 und 5 beide each und reset sind By-Referenz-Funktionen. Das $array hat ein refcount=2 wenn es an sie weitergegeben wird, muss es dupliziert werden. So wie foreach wird wieder an einem separaten Array arbeiten.

Beispiele: Effekte von current in foreach

Ein guter Weg, um die verschiedenen Duplikationsverhalten zu zeigen, ist das Verhalten der current() Funktion innerhalb einer foreach-Schleife. Betrachten Sie dieses Beispiel:

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

Hier sollten Sie das wissen current() ist eine by-ref-Funktion (eigentlich: prefer-ref), obwohl sie das Array nicht verändert. Es muss sein, um mit all den anderen Funktionen wie next welche sind alle by-ref. By-Reference Passing impliziert, dass das Array getrennt werden muss und somit $array und das Foreach-Array wird anders sein. Der Grund, den du bekommst 2 Anstatt von 1 ist auch oben erwähnt: foreach rückt den Array-Zeiger vor Vor Ausführen des Benutzercodes, nicht danach. Obwohl der Code das erste Element ist, hat foreach bereits den Zeiger auf das zweite Element gesetzt.

Jetzt versuchen wir 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, also wird das Array nicht kopiert (genau wie oben). Aber jetzt, da es eine Referenz ist, muss das Array nicht mehr dupliziert werden, wenn es an den By-Ref übergeben wird current() Funktion. So current() und foreach arbeiten auf dem gleichen Array. Sie sehen das off-by-one-Verhalten, aufgrund der Art und Weise foreachrückt den Zeiger vor.

Sie erhalten das gleiche Verhalten bei der By-Ref-Iteration:

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

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

Eine weitere kleine Variation, dieses Mal werden wir das Array einer anderen Variablen zuweisen:

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

Hier der Refcount des $array ist 2, wenn die Schleife gestartet wird, also müssen wir einmal die Duplikation im Voraus machen. So $array und die von foreach verwendete Anordnung wird von Anfang an völlig getrennt sein. Deshalb erhalten Sie die Position des IAP, wo auch immer es vor der Schleife war (in diesem Fall war es an der ersten Position).

Beispiele: Änderung während der Iteration

Der Versuch, Änderungen während der Iteration zu berücksichtigen, ist der Ausgangspunkt für alle unsere Probleme, daher dient es dazu, einige Beispiele für diesen Fall zu betrachten.

Stellen Sie sich diese verschachtelten Schleifen über dasselbe Array vor (wobei By-Ref-Iteration verwendet wird, um sicherzustellen, dass es sich wirklich um die gleiche handelt):

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 das (1, 2) fehlt in der Ausgabe, weil Element 1 wurde entfernt. Was wahrscheinlich unerwartet ist, ist, dass die äußere Schleife nach dem ersten Element stoppt. Warum das?

Der Grund dafür ist der oben beschriebene Nested-Loop-Hack: Bevor der Schleifenkörper ausgeführt wird, wird die aktuelle IAP-Position und der Hash in ein gesichert HashPointer. Nach dem Schleifenkörper wird es wiederhergestellt, aber nur, wenn das Element noch existiert, andernfalls wird stattdessen die aktuelle IAP-Position (was auch immer es sein mag) verwendet. Im obigen Beispiel ist dies genau der Fall: Das aktuelle Element der äußeren Schleife wurde entfernt, also wird es den IAP verwenden, der bereits von der inneren Schleife als abgeschlossen markiert wurde!

Eine weitere Folge der HashPointer Sicherungs- und Wiederherstellungsmechanismus sind jedoch Änderungen am IAP reset() etc. beeinflussen normalerweise nicht foreach. Der folgende Code wird beispielsweise so ausgeführt, als ob der reset() waren überhaupt nicht anwesend:

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

Der Grund ist, dass, während reset() Ändert die IAP vorübergehend, wird sie nach dem Schleifenkörper auf das aktuelle foreach-Element zurückgesetzt. Zwingen reset() Um die Schleife zu beeinflussen, müssen Sie zusätzlich das aktuelle Element entfernen, damit der Backup / Restore-Mechanismus 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 gesund. Der wahre Spaß beginnt, wenn Sie sich daran erinnern, dass die HashPointer restore verwendet einen Zeiger auf das Element und seinen Hash, um festzustellen, ob es noch existiert. Aber: Hashes haben Kollisionen und Zeiger können wiederverwendet werden! Dies bedeutet, dass wir mit einer sorgfältigen Auswahl der Array-Schlüssel machen können foreach glaube, dass ein Element, das entfernt wurde, immer noch existiert, so dass es direkt zu ihm springen wird. 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 erwarten 1, 1, 3, 4 nach den vorherigen Regeln. Was passiert ist das? 'FYFY' hat den gleichen Hash wie das entfernte Element 'EzFY'und der Zuordner verwendet zufällig denselben Speicherplatz, um das Element zu speichern. So springt foreach direkt auf das neu eingefügte Element und verkürzt so die Schleife.

Ersetzen der iterierten Entität während der Schleife

Ein letzter seltsamer Fall, den ich erwähnen möchte, ist, dass PHP es erlaubt, die iterierte Entität während der Schleife zu ersetzen. So können Sie beginnen, auf einem Array zu iterieren und es dann durch ein anderes Array in der Mitte zu ersetzen. Oder beginne mit der Iteration eines Arrays und ersetze es dann 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, beginnt PHP damit, die andere Entität von Anfang an zu durchlaufen, sobald die Substitution stattgefunden hat.

PHP 7

Hashtable-Iteratoren

Wenn Sie sich noch daran erinnern, bestand das Hauptproblem bei der Array-Iteration in der Beseitigung von Elementen in der Mitte der Iteration. 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 simultane foreach-Schleifen zu unterstützen und interagieren mit reset() usw. darüber hinaus.

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 dann haben sie die gleiche Semantik wie der IAP: Wenn ein Array-Element entfernt wird, werden alle auf dieses Element hashtable-Iteratoren zum nächsten Element weitergeleitet.

Dies bedeutet, dass foreach den IAP nicht mehr verwenden wird überhaupt. Die foreach - Schleife hat absolut keinen Einfluss auf die Ergebnisse von current() etc. und sein eigenes Verhalten wird niemals durch Funktionen wie beeinflusst werden reset() etc.

Array-Duplizierung

Eine weitere wichtige Änderung zwischen PHP 5 und PHP 7 betrifft die Array-Duplizierung. Jetzt, da die IAP nicht mehr verwendet wird, führt die By-Value-Array-Iteration in allen Fällen nur eine Refcount-Inkrementierung durch (anstatt das Array zu duplizieren). Wenn das Array während der foreach-Schleife geändert wird, findet an diesem Punkt eine Duplizierung statt (gemäß copy-on-write), und foreach arbeitet weiter am alten Array.

In den meisten Fällen ist diese Änderung transparent und hat keinen anderen Effekt als eine bessere Leistung. Es gibt jedoch eine Gelegenheit, bei der es zu einem anderen Verhalten kommt, nämlich wenn 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 war die by-value Iteration von Referenz-Arrays ein Sonderfall. In diesem Fall trat keine Duplizierung auf, so dass alle Änderungen des Arrays während der Iteration von der Schleife reflektiert würden. In PHP 7 ist dieser spezielle Fall weg: Eine by-value Iteration eines Arrays wird immer Arbeiten Sie an den Originalelementen, ohne Änderungen während der Schleife zu berücksichtigen.

Dies gilt natürlich nicht für die Referenz-Iteration. Wenn Sie iterieren, werden alle Änderungen von der Schleife übernommen. Interessanterweise gilt das Gleiche für die Iteration von einfachen Objekten mit Werten:

$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 Semantik von Objekten durch Handle wider (d. H. Sie verhalten sich selbst in Nebenwertkontexten 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 ist das Refcounting- und Duplikationsverhalten zwischen PHP 5 und PHP 7 genau gleich).

  • Testfall 3 ändert sich: Foreach verwendet das IAP nicht mehr each() ist von der Schleife nicht betroffen. Es wird die gleiche Ausgabe vorher und nachher haben.

  • Testfälle 4 und 5 bleiben gleich: each() und reset() Wird das Array vor dem Ändern der IAP duplizieren, während Foreach weiterhin das ursprüngliche Array verwendet. (Nicht dass die IAP-Änderung wichtig gewesen wäre, selbst wenn das Array geteilt wurde.)

Die zweite Reihe von Beispielen bezog sich auf das Verhalten von current() unter verschiedenen Referenz / Refcounting-Konfigurationen. Das macht keinen Sinn mehr, wie current() ist von der Schleife völlig unbeeinflusst, so dass der Rückgabewert immer gleich bleibt.

Wir sehen jedoch einige interessante Änderungen, wenn wir Änderungen während der Iteration betrachten. 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 nicht mehr nach der ersten Iteration ab. Der Grund dafür ist, dass beide Schleifen nun völlig separate Hashtable-Iteratoren haben und keine Kreuzkontamination beider Schleifen mehr durch einen gemeinsamen IAP besteht.

Ein weiterer seltsamer Randfall, der jetzt behoben wird, ist der seltsame Effekt, den Sie erhalten, wenn Sie Elemente entfernen und hinzufügen, die denselben Hashwert 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 auf das neue Element, weil es so aussah, als wäre es dasselbe wie das remove-Element (aufgrund von kollidierendem Hash und Zeiger). Da wir uns für nichts mehr auf den Element-Hash verlassen, ist dies kein Problem mehr.


1378
2018-02-13 13:21



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

Der Zuweisungsoperator für die Arrays in PHP funktioniert eher wie ein fauler Klon. Wenn Sie eine Variable einer anderen zuweisen, die ein Array enthält, wird das Array im Gegensatz zu den meisten anderen Sprachen geklont. Das eigentliche Klonen wird jedoch nicht durchgeführt, wenn es nicht benötigt wird. Dies bedeutet, dass der Klon nur dann 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.

Kommen Sie zu Ihren Testfällen zurück, können Sie sich das leicht vorstellen foreach erstellt eine Art Iterator mit einem Verweis auf das Array. Diese Referenz funktioniert genau wie die Variable $b in meinem Beispiel. Der Iterator mit der Referenz lebt jedoch nur während der Schleife, und dann werden 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 vor sich geht!

Hier ist ein ausgezeichneter Artikel für einen weiteren Nebeneffekt dieses Copy-on-Write-Verhaltens: Der Ternary Operator von PHP: Schnell oder nicht?


97
2018-04-07 20:43



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

ein) foreach arbeitet an der Prospected Kopie des ursprünglichen Arrays.     Es bedeutet, dass foreach () SHARED-Datenspeicher hat, bis oder außer a prospected copy ist     nicht erstellt foreach Anmerkungen / Benutzerkommentare.

b) Was löst a aus? Prospected Kopie?     Die erwartete Kopie wird basierend auf der Richtlinie erstellt copy-on-writeWann immer     Wenn ein an foreach () übergebenes Array geändert wird, wird ein Klon des ursprünglichen Arrays erstellt.

c) Der originale Array- und foreach () Iterator wird haben DISTINCT SENTINEL VARIABLESdas heißt, eine für die ursprüngliche Anordnung und andere für foreach; Sehen Sie den Testcode unten. SPL , Iteratoren, und Array-Iterator.

Stapelüberlauffrage Wie stelle ich sicher, dass der Wert in einer foreach-Schleife in PHP zurückgesetzt wird? adressiert die Fälle (3,4,5) Ihrer Frage.

Das folgende Beispiel zeigt, dass each () und reset () NICHT betroffen sind SENTINEL Variablen (for example, the current index variable) des foreach () Iterators.

$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

34
2018-04-07 21:03



HINWEIS für PHP 7

Um diese Antwort zu aktualisieren, hat sie eine gewisse Popularität erlangt: Diese Antwort gilt ab PHP 7 nicht mehr.Rückwärts inkompatible Änderungen"In PHP 7 arbeitet foreach an der Kopie des Arrays, so dass alle Änderungen am Array selbst nicht in der foreach-Schleife berücksichtigt werden. Weitere Details finden Sie unter dem Link.

Erklärung (Zitat aus php.net):

Die erste Form läuft über das durch array_expression angegebene Array. Auf jeder   Iteration wird der Wert des aktuellen Elements $ value und zugewiesen   Der interne Array-Zeiger wird um Eins vorgerückt (also beim nächsten   Iteration, werden Sie auf das nächste Element schauen).

Also, in Ihrem ersten Beispiel haben Sie nur ein Element im Array, und wenn der Zeiger verschoben wird, existiert das nächste Element nicht, also nachdem Sie ein neues Element foreach hinzugefügt haben, da es bereits entschieden hat, dass es das letzte Element ist.

In Ihrem zweiten Beispiel beginnen Sie mit zwei Elementen, und foreach loop ist nicht das letzte Element, daher wertet es das Array bei der nächsten Iteration aus und stellt somit fest, dass ein neues Element im Array vorhanden ist.

Ich glaube, das ist alles eine Konsequenz von Bei jeder Iteration Teil der Erklärung in der Dokumentation, was wahrscheinlich bedeutet, dass foreach macht alle Logik, bevor es den Code aufruft {}.

Testfall

Wenn Sie das 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 akzeptierte und durchging, weil es "in time" geändert wurde. Aber wenn du das tust:

<?
    $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
)

Was bedeutet, dass Array geändert wurde, aber seit wir es geändert haben, als die foreach war schon beim letzten Element des Arrays, "entschied", nicht mehr zu loopen, und obwohl wir ein neues Element hinzufügten, fügten wir es "zu spät" hinzu und es wurde nicht durchgeschleift.

Detaillierte Erklärung finden Sie unter Wie funktioniert PHP 'foreach' eigentlich? Das erklärt die Interna hinter diesem Verhalten.


22
2018-04-15 08:46



Laut der Dokumentation von PHP-Handbuch zur Verfügung gestellt.

Bei jeder Iteration wird der Wert des aktuellen Elements $ v und dem internen Element zugewiesen
  Der Zeiger des Arrays wird um eins erhöht (bei der nächsten Iteration sehen Sie sich das nächste Element an).

Also nach deinem ersten Beispiel:

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

$array Habe nur ein einzelnes Element, also nach der foreach Ausführung, 1 zuweisen $vund es hat kein anderes Element, um Zeiger zu bewegen

Aber in deinem zweiten Beispiel:

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

$array Habe zwei Elemente, also werte $ array jetzt die Nullindizes aus und verschiebe den Zeiger um eins. Für die erste Iteration der Schleife hinzugefügt $array['baz']=3; als durch Verweis gehen.


8
2018-04-15 09:32



Große Frage, denn viele Entwickler, selbst erfahrene, sind verwirrt, wie PHP Arrays in foreach-Schleifen behandelt. 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 verworfen. Dies ist in der Operation einer einfachen foreach-Schleife transparent. Beispielsweise:

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

Dies ergibt:

apple
banana
coconut

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

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

print_r($set);

Dies ergibt:

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

Alle Änderungen gegenüber dem Original können keine Notizen sein, tatsächlich gibt es 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 gerade gearbeitet wird. Sie können dies überschreiben, indem Sie $ item per Verweis abrufen, wie folgt:

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

Dies ergibt:

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

Es ist also evident und beobachtbar, wenn $ item by-reference betrieben wird, werden die an $ item vorgenommenen Änderungen an die Mitglieder des ursprünglichen $ set gesetzt. Die Verwendung von $ item by reference verhindert auch, dass PHP die Array-Kopie erstellt. Um dies zu testen, zeigen wir zuerst ein schnelles Skript, das die Kopie zeigt:

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

Dies ergibt:

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

Wie im Beispiel gezeigt, kopierte PHP $ set und benutzte es zur Schleife, aber wenn $ set innerhalb der Schleife verwendet wurde, fügte PHP die Variablen zum ursprünglichen Array hinzu, nicht zum kopierten Array. Im Grunde 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 fügt sie einen anderen Wert an das Ende des ursprünglichen $ set an, wobei das ursprüngliche $ set mit 6 Elementen belassen wird, aber niemals in eine Endlosschleife.

Wie wäre es jedoch, wenn wir $ item by reference verwendet hätten, wie ich bereits erwähnt habe? Ein einzelnes Zeichen, das dem obigen Test hinzugefügt wurde:

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

Ergibt eine Endlosschleife. Beachten Sie, dass es sich tatsächlich um eine Endlosschleife handelt. Sie müssen entweder das Skript selbst beenden oder darauf warten, dass auf Ihrem Betriebssystem nicht mehr genügend Arbeitsspeicher verfügbar ist. Ich habe die folgende Zeile zu meinem Skript hinzugefügt, damit PHP sehr schnell keinen Speicher mehr hat. Ich schlage vor, dass Sie dasselbe tun, wenn Sie diese Endlosschleife testen wollen:

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, um darüber zu schleifen. 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, so dass Sie nie auf Probleme stoßen werden.


5
2018-04-21 08:44



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

In foreach-Schleife ist das erste, was php tut, dass es eine Kopie des Arrays erstellt, das über iteriert werden soll. PHP iteriert dann über dieses neue copy des Arrays statt des ursprünglichen. Dies wird im folgenden Beispiel demonstriert:

<?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 es zu benutzen iterated values as a reference to the original array value auch. Dies wird im Folgenden gezeigt:

<?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: Es erlaubt nicht original array indexes als verwendet werden references.

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


5
2017-11-13 14:08