Lassen Sie mich dies voranstellen, indem Sie sagen, dass ich weiß, was foreach
ist, tut und wie ich es verwenden soll. Diese Frage betrifft die Funktionsweise unter der Motorhaube, und ich möchte keine Antworten im Sinne von "So schleifen Sie ein Array mit foreach
".
Lange habe ich angenommen, dass das foreach
mit dem Array selbst funktioniert. Dann fand ich viele Hinweise darauf, 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 über diese Angelegenheit geraten und habe nach ein wenig Experimentieren festgestellt, dass dies tatsächlich nicht 100% wahr ist.
Lassen Sie mich zeigen, was ich meine. Für die folgenden Testfälle arbeiten wir mit dem folgenden Array:
$array = array(1, 2, 3, 4, 5);
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 schieben. Aber nur um sicherzugehen, dass dies der Fall ist:
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 stützt unsere anfängliche Schlussfolgerung, dass wir während der Schleife mit einer Kopie des Quellarrays arbeiten, andernfalls würden wir die geänderten Werte während der Schleife sehen. 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 foreach
hinzudeuten , dass dies auf dem Array-Zeiger des Quell-Arrays beruht. Aber wir haben gerade bewiesen, dass wir nicht mit dem Quell-Array arbeiten , oder? Nun, nicht ganz.
// 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 Quellarray arbeiten, arbeiten wir direkt mit dem Quellarrayzeiger - die Tatsache, dass sich der Zeiger am Ende des Arrays am Ende der Schleife befindet, zeigt dies. Außer dies kann nicht wahr sein - wenn es so wäre, würde Testfall 1 für immer eine Schleife bilden.
Das PHP-Handbuch besagt außerdem:
Da foreach darauf angewiesen ist, dass der interne Array-Zeiger innerhalb der Schleife geändert wird, kann dies zu unerwartetem Verhalten führen.
Lassen Sie uns herausfinden, was dieses "unerwartete Verhalten" ist (technisch gesehen ist jedes Verhalten unerwartet, da ich nicht mehr weiß, was mich erwartet).
foreach ($array as $key => $item) {
echo "$item\n";
each($array);
}
/* Output: 1 2 3 4 5 */
foreach ($array as $key => $item) {
echo "$item\n";
reset($array);
}
/* Output: 1 2 3 4 5 */
... nichts so Unerwartetes, tatsächlich 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 ihn für mich ins Englische übersetzen könnte.
Es scheint mir, dass dies foreach
mit einer Kopie des Arrays funktioniert , aber den Array-Zeiger des Quell-Arrays nach der Schleife auf das Ende des Arrays setzt.
- Ist das richtig und die ganze Geschichte?
- Wenn nicht, was macht es dann wirklich?
- Gibt es eine Situation , in den Funktionen, die den Array Zeiger einstellen (
each()
,reset()
et al.) Während einemforeach
konnte das Ergebnis der Schleife beeinflussen?
foreach ($array as &$value)
) - PHP muss die aktuelle Position im ursprünglichen Array kennen, obwohl es tatsächlich über eine Kopie iteriert.Antworten:
foreach
unterstützt die Iteration über drei verschiedene Arten von Werten:Traversable
ObjekteIm Folgenden werde ich versuchen, genau zu erklären, wie Iteration in verschiedenen Fällen funktioniert. Bei weitem der einfachste Fall sind
Traversable
Objekte, da es sich bei diesenforeach
im Wesentlichen nur um Syntaxzucker für Code in dieser Richtung handelt:Bei internen Klassen werden tatsächliche Methodenaufrufe vermieden, indem eine interne API verwendet wird, die im Wesentlichen nur die
Iterator
Schnittstelle 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 in dieser Reihenfolge durchlaufen werden (die der Einfügereihenfolge entspricht, solange Sie so etwas nicht verwendet haben
sort
). Dies steht im Gegensatz zu einer Iteration durch die natürliche Reihenfolge der Schlüssel (wie Listen in anderen Sprachen häufig funktionieren) oder einer überhaupt nicht definierten Reihenfolge (wie Wörterbücher in anderen Sprachen häufig funktionieren).Gleiches gilt auch für Objekte, da die Objekteigenschaften als ein anderes (geordnetes) Wörterbuch angesehen werden können, das Eigenschaftsnamen ihren Werten zuordnet, sowie eine gewisse Sichtbarkeitsbehandlung. In den meisten Fällen werden die Objekteigenschaften nicht auf diese eher ineffiziente Weise 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 von einfachen Objekten spreche).
So weit, ist es gut. Das Durchlaufen eines Wörterbuchs kann nicht allzu 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:
foreach ($arr as &$v)
dann$arr
wird in eine Referenz gedreht und man kann es während der Iteration ändern.$ref =& $arr; foreach ($ref as $v)
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 zu verfolgen, 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 Randfällen führte, während der komplexere Ansatz von PHP 7 zu einem vorhersehbareren und konsistenteren Verhalten führt.
Als letzte Vorbemerkung sollte angemerkt werden, dass PHP Referenzzählung und Copy-on-Write verwendet, um den Speicher zu verwalten. Das heißt, wenn Sie einen Wert "kopieren", verwenden Sie den alten Wert einfach wieder und erhöhen seinen Referenzzähler (Refcount). Erst wenn Sie eine Änderung vornehmen, wird eine echte Kopie (als "Duplizierung" bezeichnet) erstellt. Sehen Sie werden belogen für eine umfangreichere 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 es stattdessen zum nächsten Element weitergeleitet.
Während
foreach
der IAP verwendet wird, gibt es eine zusätzliche Komplikation: Es gibt nur einen IAP, aber ein Array kann Teil mehrererforeach
Schleifen sein:Um zwei gleichzeitige Schleifen mit nur einem internen Array-Zeiger zu unterstützen, führen Sie
foreach
die folgenden Spielereien aus: Bevor der Schleifenkörper ausgeführt wird,foreach
wird ein Zeiger auf das aktuelle Element und seinen Hash in einem Per-Foreach gesichertHashPointer
. 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 es nur dort, wo sich der IAP gerade befindet. Dieses Schema funktioniert meistens irgendwie, aber es gibt eine Menge seltsames Verhalten, das man daraus machen kann, von denen ich einige unten demonstrieren werde.Array-Duplizierung
Der IAP ist ein sichtbares Merkmal eines Arrays (das durch die
current
Funktionsfamilie verfügbar gemacht wird ), da solche Änderungen am IAP als Änderungen unter der Semantik des Kopierens beim Schreiben gelten. Dies bedeutet leider, dassforeach
in vielen Fällen das Array, über das es iteriert, dupliziert werden muss. Die genauen Bedingungen sind:refcount
1 ist, wird das Array nicht freigegeben und wir können es direkt ändern.Wenn das Array nicht dupliziert wird (is_ref = 0, refcount = 1), wird nur sein Array
refcount
inkrementiert (*). Wennforeach
eine Referenz verwendet wird, wird das (möglicherweise duplizierte) Array in eine Referenz umgewandelt.Betrachten Sie diesen Code als Beispiel für eine Duplizierung:
Hier
$arr
wird dupliziert zu verhindern , auf IAP ändert$arr
Auslaufen zu$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 unglücklich 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 des
refcount
hier klingt harmlos, verstößt jedoch gegen die COW-Semantik (Copy-on-Write): Dies bedeutet, dass wir den IAP eines refcount = 2-Arrays ändern werden, während COW vorschreibt, dass Änderungen nur für refcount = durchgeführt werden können 1 Werte. Diese Verletzung führt zu einer vom Benutzer sichtbaren Verhaltensänderung (während eine COW normalerweise transparent ist), da die IAP-Änderung im iterierten Array beobachtet werden kann - jedoch nur bis zur ersten Nicht-IAP-Änderung im Array. Stattdessen wären die drei "gültigen" Optionen a) gewesen, um immer zu duplizieren, b) nicht zu erhöhenrefcount
und somit zuzulassen, dass das iterierte Array in der Schleife willkürlich geändert wird, oder c) den IAP überhaupt nicht zu verwenden (das PHP 7 Lösung).Positionsvorschubreihenfolge
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:
Da
foreach
es sich jedoch um eine ganz besondere Schneeflocke handelt, geht man etwas anders vor:Der Array-Zeiger wird nämlich bereits vorwärts bewegt, bevor der Schleifenkörper ausgeführt wird. Dies bedeutet, dass sich
$i
der IAP bereits am Element befindet , während der Schleifenkörper am Element arbeitet$i+1
. Dies ist der Grund, warum Codebeispiele, die Änderungen während der Iteration zeigen, immerunset
das nächste Element und nicht das aktuelle Element sind.Beispiele: Ihre Testfälle
Die drei oben beschriebenen Aspekte sollen Ihnen einen weitgehend vollständigen Eindruck von den Besonderheiten der
foreach
Implementierung 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
$array
beginnt mit refcount = 1, sodass es nicht dupliziert wird durchforeach
: Nur dasrefcount
wird inkrementiert. Wenn der Schleifenkörper anschließend das Array ändert (das an diesem Punkt refcount = 2 hat), tritt die Duplizierung an diesem Punkt auf. Foreach wird weiterhin an einer unveränderten Kopie von arbeiten$array
.In Testfall 3 wird das Array erneut nicht dupliziert,
foreach
wodurch der IAP der$array
Variablen geändert wird. Am Ende der Iteration ist der IAP NULL (was bedeutet, dass die Iteration durchgeführt wurde), waseach
durch Zurückgeben angezeigt wirdfalse
.In Testfällen 4 und 5 , die beide
each
undreset
sind Nebenreferenzfunktionen. Das$array
hat ein,refcount=2
wenn es an sie übergeben wird, also muss es dupliziert werden. Als solchesforeach
wird wieder an einem separaten Array gearbeitet.Beispiele: Auswirkungen von
current
in foreachEine gute Möglichkeit, die verschiedenen Duplizierungsverhalten zu zeigen, besteht darin, das Verhalten der
current()
Funktion innerhalb einerforeach
Schleife zu beobachten . Betrachten Sie dieses Beispiel:Hier sollten Sie wissen, dass
current()
es sich um eine By-Ref-Funktion handelt (eigentlich: Prefer-Ref), obwohl das Array dadurch nicht geändert wird. Es muss sein, um mit all den anderen Funktionen, wie sienext
alle by-ref sind, gut zu spielen. Das Übergeben von Referenzen impliziert, dass das Array getrennt werden muss und daher$array
und dasforeach-array
wird anders sein. Der Grund, den Sie2
anstelle von erhalten,1
ist auch oben erwähnt:foreach
Erweitert den Array-Zeiger vor dem Ausführen des Benutzercodes, nicht danach. Obwohl sich der Code im ersten Element befindet, wurdeforeach
der Zeiger bereits auf das zweite Element verschoben.Versuchen wir nun eine kleine Modifikation:
Hier haben wir den Fall is_ref = 1, so dass das Array nicht kopiert wird (genau wie oben). Jetzt, da es sich um eine Referenz handelt, muss das Array nicht mehr dupliziert werden, wenn es an die by-ref-
current()
Funktion übergeben wird. Alsocurrent()
undforeach
arbeite am selben Array. Sie sehen jedoch immer noch das Off-by-One-Verhalten, daforeach
der Zeiger vorwärts bewegt wird.Sie erhalten das gleiche Verhalten, wenn Sie eine Iteration nach Referenz durchführen:
Hier ist der wichtige Teil, dass foreach
$array
ein 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, dieses Mal weisen wir das Array einer anderen Variablen zu:
Hier ist der Refcount von
$array
2, wenn die Schleife gestartet wird, also müssen wir die Duplizierung einmal im Voraus durchführen. Somit ist$array
das von foreach verwendete Array von Anfang an vollständig getrennt. Aus diesem Grund erhalten Sie die Position des IAP an der Stelle, an der sie sich vor der Schleife befand (in diesem Fall an der ersten Position).Beispiele: Änderung während der Iteration
Der Versuch, Änderungen während der Iteration zu berücksichtigen, ist der Ursprung all unserer foreach-Probleme. Daher werden 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 dasselbe ist):
Der erwartete Teil hier ist, dass
(1, 2)
er in der Ausgabe fehlt, weil das Element1
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 aktuelle Hash in a gesichert
HashPointer
. Nach dem Schleifenkörper wird er wiederhergestellt, jedoch nur, wenn das Element noch vorhanden ist. Andernfalls wird stattdessen die aktuelle IAP-Position (wie auch immer) 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 fertig markiert wurde!Eine weitere Konsequenz des
HashPointer
Backup + Restore-Mechanismus ist, dass Änderungen am IAP überreset()
usw. normalerweise keine Auswirkungen habenforeach
. Der folgende Code wird beispielsweise so ausgeführt, als ob derreset()
überhaupt nicht vorhanden wäre:Der Grund dafür ist, dass
reset()
der IAP zwar vorübergehend geändert wird, jedoch nach dem Schleifenkörper im aktuellen foreach-Element wiederhergestellt wird. Umreset()
eine Auswirkung auf die Schleife zu erzwingen , müssen Sie zusätzlich das aktuelle Element entfernen, damit der Sicherungs- / Wiederherstellungsmechanismus fehlschlägt:Aber diese Beispiele sind immer noch vernünftig. Der wahre Spaß beginnt, wenn Sie sich daran erinnern, dass die
HashPointer
Wiederherstellung einen Zeiger auf das Element und seinen Hash verwendet, um festzustellen, ob es noch vorhanden ist. Aber: Hashes haben Kollisionen und Zeiger können wiederverwendet werden! Dies bedeutet, dass wir bei sorgfältiger Auswahl der Array-Schlüssel davon ausgehen können,foreach
dass ein entferntes Element noch vorhanden ist, sodass es direkt dorthin springt. Ein Beispiel:Hier sollten wir normalerweise die Ausgabe
1, 1, 3, 4
gemäß den vorherigen Regeln erwarten . Was passiert, ist, dass'FYFY'
es denselben Hash wie das entfernte Element'EzFY'
hat und der Allokator zufällig denselben Speicherort zum Speichern des Elements wiederverwendet. Foreach springt also direkt zum neu eingefügten 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 Sie mit PHP die iterierte Entität während der Schleife ersetzen können. Sie können also mit der Iteration eines Arrays beginnen und es dann zur Hälfte durch ein anderes Array ersetzen. Oder beginnen Sie mit der Iteration eines Arrays und ersetzen Sie es durch ein Objekt:
Wie Sie in diesem Fall sehen können, beginnt PHP von Anfang an mit der Iteration der anderen Entität, sobald die Ersetzung erfolgt ist.
PHP 7
Hashtable-Iteratoren
Wenn Sie sich noch erinnern, bestand das Hauptproblem bei der Array-Iteration darin, wie Elemente während der Iteration entfernt werden. 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 gleichzeitige foreach-Schleifen und die Interaktion mit
reset()
usw. darüber hinaus zu unterstützen.PHP 7 verwendet einen anderen Ansatz, nämlich die Erstellung einer beliebigen Anzahl externer, sicherer Hashtabellen-Iteratoren. Diese Iteratoren müssen im Array registriert sein. Ab diesem Zeitpunkt haben sie dieselbe Semantik wie der IAP: Wenn ein Array-Element entfernt wird, werden alle Hashtabellen-Iteratoren, die auf dieses Element verweisen, zum nächsten Element weitergeleitet.
Dies bedeutet, dass
foreach
der IAP überhaupt nicht mehr verwendet wird . Dieforeach
Schleife hat absolut keinen Einfluss auf die Ergebnisse voncurrent()
usw. und ihr eigenes Verhalten wird niemals durch Funktionen wiereset()
usw. beeinflusst.Array-Duplizierung
Eine weitere wichtige Änderung zwischen PHP 5 und PHP 7 betrifft die Duplizierung von Arrays. Nachdem der IAP nicht mehr verwendet wird, führt die Array-Iteration nach Wert
refcount
in allen Fällen nur noch ein Inkrement durch (anstatt das Array zu duplizieren). Wenn das Array während derforeach
Schleife geändert wird, tritt zu diesem Zeitpunkt eine Duplizierung auf (gemäß Copy-on-Write) undforeach
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 in dem Fall, in dem das Array zuvor eine Referenz war:
Bisher war die Iteration von Referenz-Arrays nach Wert Sonderfälle. In diesem Fall ist keine Duplizierung aufgetreten, sodass alle Änderungen des Arrays während der Iteration von der Schleife wiedergegeben werden. In PHP 7 ist dieser Sonderfall weg: Eine Iteration eines Arrays nach Wert arbeitet immer an den ursprünglichen Elementen, ohne Änderungen während der Schleife zu berücksichtigen.
Dies gilt natürlich nicht für die Iteration nach Referenz. Wenn Sie per Referenz iterieren, werden alle Änderungen von der Schleife übernommen. Interessanterweise gilt das Gleiche für die By-Value-Iteration einfacher Objekte:
Dies spiegelt die By-Handle-Semantik von Objekten wider (dh 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 dieselbe Ausgabe bei: Die Array-Iteration nach Wert arbeitet immer an den ursprünglichen Elementen. (In diesem Fall ist das gerade
refcounting
und doppelte Verhalten zwischen PHP 5 und PHP 7 genau gleich.)Änderungen an Testfall 3:
Foreach
Verwendet den IAP nicht mehr undeach()
ist daher von der Schleife nicht betroffen. Es wird vorher und nachher die gleiche Ausgabe haben.Testfälle 4 und 5 gleich bleiben:
each()
undreset()
das Array duplizieren , bevor die IAP zu ändern, währendforeach
immer noch das Original - Array verwendet. (Nicht, dass die IAP-Änderung von Bedeutung gewesen wäre, selbst wenn das Array gemeinsam genutzt worden wäre.)Die zweite Reihe von Beispielen bezog sich auf das Verhalten
current()
unter verschiedenenreference/refcounting
Konfigurationen. Dies ist nicht mehr sinnvoll, dacurrent()
es von der Schleife völlig unberührt bleibt und der Rückgabewert immer gleich bleibt.Wir erhalten jedoch einige interessante Änderungen, wenn wir Änderungen während der Iteration berücksichtigen. Ich hoffe, Sie finden das neue Verhalten vernünftiger. Das erste Beispiel:
Wie Sie sehen, wird die äußere Schleife nach der ersten Iteration nicht mehr abgebrochen. Der Grund dafür ist, dass beide Schleifen jetzt vollständig separate Hashtabellen-Iteratoren haben und keine Kreuzkontamination mehr beider Schleifen durch einen gemeinsam genutzten IAP mehr besteht.
Ein weiterer seltsamer Randfall, der jetzt behoben ist, ist der seltsame Effekt, den Sie erhalten, wenn Sie Elemente entfernen und hinzufügen, die zufällig denselben Hash haben:
Zuvor sprang der HashPointer-Wiederherstellungsmechanismus direkt auf das neue Element, da es "aussah", als wäre es dasselbe wie das entfernte Element (aufgrund von kollidierendem Hash und Zeiger). Da wir uns für nichts mehr auf den Element-Hash verlassen, ist dies kein Problem mehr.
quelle
$foo = $array
vor der Schleife;)Bucket
s sind Teil einer doppelt verknüpften Liste für Hash-Kollisionen und auch Teil einer doppelt verknüpften Liste für Bestellungen;)iterate($outerArr);
und nichtiterate($arr);
irgendwo.In Beispiel 3 ändern Sie das Array nicht. In allen anderen Beispielen ändern Sie entweder den Inhalt oder den internen Array-Zeiger. Dies ist bei PHP- Arrays aufgrund der Semantik des Zuweisungsoperators wichtig .
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 Sprachen geklont. Das eigentliche Klonen wird jedoch nur durchgeführt, wenn es benötigt wird. Dies bedeutet, dass der Klon nur stattfindet, wenn eine der Variablen geändert wird (Copy-on-Write).
Hier ist ein Beispiel:
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 wird. Diese Referenz funktioniert genau wie die Variable$b
in meinem Beispiel. Der Iterator und die Referenz leben jedoch nur während der Schleife, und dann werden beide verworfen. Jetzt können Sie sehen, dass das Array in allen Fällen außer 3 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 Copy-on-Write-Verhaltens: Der ternäre PHP-Operator: Schnell oder nicht?
quelle
each()
am Ende des ersten Testfall, wo wir sehen , dass der Array - Zeiger der ursprünglichen Array zeigt auf das zweite Element, da das Array geändert wurde während die erste Iteration. Dies scheint auch zu demonstrieren, dassforeach
der Array-Zeiger vor dem Ausführen des Codeblocks der Schleife weiter bewegt wird, was ich nicht erwartet hatte - ich hätte gedacht, dass dies am Ende der Fall sein würde. Vielen Dank, das klärt es für mich schön auf.Einige Punkte, die Sie bei der Arbeit beachten sollten
foreach()
:a)
foreach
arbeitet an der voraussichtlichen Kopie des ursprünglichen Arrays.foreach()
Dies bedeutet, dass die gemeinsame Nutzung von Daten so lange gespeichert wird, bis für jede Notizen / Benutzerkommentare eine erstelltprospected copy
wird .b) Was löst eine prospektierte Kopie aus ? Eine prospektierte Kopie wird basierend auf der Richtlinie erstellt
copy-on-write
, dh, wann immer ein übergebenes Arrayforeach()
geändert wird, wird ein Klon des ursprünglichen Arrays erstellt.c) Das ursprüngliche Array und der
foreach()
Iterator habenDISTINCT SENTINEL VARIABLES
, dh eines für das ursprüngliche Array und eines fürforeach
; Siehe den Testcode unten. SPL , Iteratoren und Array-Iterator .Frage zum Stapelüberlauf Wie kann sichergestellt werden, dass der Wert in einer 'foreach'-Schleife in PHP zurückgesetzt wird? befasst sich mit den Fällen (3,4,5) Ihrer Frage.
Das folgende Beispiel zeigt, dass each () und reset () KEINE
SENTINEL
Variablen(for example, the current index variable)
desforeach()
Iterators beeinflussen.Ausgabe:
quelle
foreach
arbeitet mit einer potenziellen Kopie des Arrays, erstellt jedoch nur dann die eigentliche Kopie, wenn sie benötigt wird.foreach
das Array 100% der Zeit kopiert wird. Ich bin gespannt zu wissen. Vielen Dank fürfor
oder benötigt wirdforeach
. Sie werden keinen signifikanten Unterschied zwischen den beiden feststellen, da keine tatsächliche Kopie stattfindet.SHARED data storage
bis oder bis reserviert istcopy-on-write
, aber (aus meinem Code-Snippet) es ist offensichtlich, dass es immer ZWEI Sätze vonSENTINEL variables
einem für denoriginal array
anderen und für den anderen geben wirdforeach
. Danke, das macht SinnHINWEIS FÜR PHP 7
So aktualisieren Sie diese Antwort, da sie an Popularität gewonnen hat: Diese Antwort gilt ab PHP 7 nicht mehr. Wie unter " Rückwärts inkompatible Änderungen " erläutert , funktioniert in PHP 7 foreach die Kopie des Arrays, sodass alle Änderungen am Array selbst vorgenommen werden werden nicht in jeder Schleife wiedergegeben. Weitere Details unter dem Link.
Erklärung (Zitat aus php.net ):
In Ihrem ersten Beispiel haben Sie also nur ein Element im Array, und wenn der Zeiger verschoben wird, ist das nächste Element nicht vorhanden. Nachdem Sie also ein neues Element hinzugefügt haben, endet jedes Element, da es bereits "entschieden" hat, dass es das letzte Element ist.
In Ihrem zweiten Beispiel beginnen Sie mit zwei Elementen, und foreach-Schleife befindet sich nicht am letzten Element, sodass das Array bei der nächsten Iteration ausgewertet wird und somit erkannt wird, dass das Array ein neues Element enthält.
Ich glaube, dass dies alles eine Folge von Bei jedem Iterationsteil der Erklärung in der Dokumentation ist, was wahrscheinlich bedeutet, dass
foreach
die gesamte Logik ausgeführt wird, bevor der Code aufgerufen wird{}
.Testfall
Wenn Sie dies ausführen:
Sie erhalten diese Ausgabe:
Dies bedeutet, dass es die Änderung akzeptiert und durchlaufen hat, weil es "rechtzeitig" geändert wurde. Aber wenn Sie dies tun:
Sie erhalten:
Das bedeutet, dass das Array geändert wurde, aber da wir es geändert haben, als sich das
foreach
bereits am letzten Element des Arrays befand, hat es "beschlossen", keine Schleife mehr durchzuführen, und obwohl wir ein neues Element hinzugefügt haben, haben wir es "zu spät" hinzugefügt und es wurde nicht durchgeschleift.Eine ausführliche Erklärung finden Sie unter Wie funktioniert PHP 'foreach' tatsächlich? Das erklärt die Interna hinter diesem Verhalten.
quelle
Gemäß der Dokumentation im PHP-Handbuch.
Also wie in Ihrem ersten Beispiel:
$array
haben nur ein einzelnes Element, also gemäß der foreach-Ausführung 1 zuweisen$v
und es hat kein anderes Element, um den Zeiger zu bewegenAber in Ihrem zweiten Beispiel:
$array
Haben Sie zwei Elemente, also wertet $ array jetzt die Nullindizes aus und bewegt den Zeiger um eins. Für die erste Iteration der Schleife$array['baz']=3;
als Referenzübergabe hinzugefügt .quelle
Gute Frage, denn viele Entwickler, auch erfahrene, sind verwirrt darüber, wie PHP Arrays in foreach-Schleifen verarbeitet. 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 beim Betrieb einer einfachen foreach-Schleife transparent. Zum Beispiel:
Dies gibt aus:
Die Kopie wird also erstellt, aber der Entwickler bemerkt es nicht, da 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 am Ende nicht geändert werden:
Dies gibt aus:
Änderungen gegenüber dem Original können keine Hinweise 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 angezeigt wird, an der gearbeitet wird. Sie können dies überschreiben, indem Sie $ item als Referenz abrufen, wie folgt:
Dies gibt aus:
Es ist also offensichtlich und beobachtbar, dass die an $ item vorgenommenen Änderungen an den Mitgliedern des ursprünglichen $ -Satzes vorgenommen werden, wenn $ item als Referenz bearbeitet wird. Die Verwendung von $ item als Referenz verhindert auch, dass PHP die Array-Kopie erstellt. Um dies zu testen, zeigen wir zunächst ein kurzes Skript, das die Kopie demonstriert:
Dies gibt aus:
Wie im Beispiel gezeigt, hat PHP $ set kopiert und zum Schleifen verwendet. Wenn jedoch $ set innerhalb der Schleife verwendet wurde, fügte PHP die Variablen dem ursprünglichen Array hinzu, nicht dem kopierten Array. Grundsätzlich verwendet PHP das kopierte Array nur zur Ausführung der Schleife und zur Zuweisung von $ item. Aus diesem Grund wird die obige Schleife nur dreimal ausgeführt und jedes Mal wird ein anderer Wert an das Ende des ursprünglichen $ -Satzes angehängt, wobei der ursprüngliche $ -Satz 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:
Ergibt eine Endlosschleife. Beachten Sie, dass dies tatsächlich eine Endlosschleife ist. Sie müssen das Skript entweder selbst beenden oder warten, bis Ihr Betriebssystem nicht mehr über genügend Arbeitsspeicher verfügt. Ich habe meinem Skript die folgende Zeile hinzugefügt, damit PHP sehr schnell keinen Speicher mehr hat. Ich schlage vor, dass Sie dasselbe tun, wenn Sie diese Endlosschleifentests ausführen:
In diesem vorherigen Beispiel mit der Endlosschleife sehen wir den Grund, warum PHP geschrieben wurde, um eine Kopie des Arrays zum Überlaufen zu erstellen. Wenn eine Kopie nur von der Struktur des Schleifenkonstrukts selbst erstellt und verwendet wird, bleibt das Array während der Ausführung der Schleife statisch, sodass Sie nie auf Probleme stoßen.
quelle
PHP foreach loop kann mit
Indexed arrays
,Associative arrays
und verwendet werdenObject public variables
.In der foreach-Schleife erstellt php als erstes eine Kopie des Arrays, über das iteriert werden soll. PHP iteriert dann über dieses neue
copy
Array und nicht über das ursprüngliche. Dies wird im folgenden Beispiel gezeigt:Außerdem erlaubt PHP auch die Verwendung
iterated values as a reference to the original array value
. Dies wird unten gezeigt:Hinweis: Es
original array indexes
darf nicht als verwendet werdenreferences
.Quelle: http://dwellupper.io/post/47/understanding-php-foreach-loop-with-examples
quelle
Object public variables
ist falsch oder bestenfalls irreführend. Sie können ein Objekt in einem Array nicht ohne die richtige Schnittstelle (z. B. Traversible) verwenden, und wenn Sie dies tunforeach((array)$obj ...
, arbeiten Sie tatsächlich mit einem einfachen Array, nicht mehr mit einem Objekt.