loops mehrdimensionales - Wie funktioniert PHP 'foreach' eigentlich?




array object (7)

Lassen Sie mich dies mit der Aussage foreach , dass ich weiß, was foreach ist, tut und wie man es benutzt. Diese Frage betrifft, wie es unter der Motorhaube funktioniert, und ich will keine Antworten in Richtung "das ist, wie Sie ein Array mit foreach ".

Lange Zeit nahm ich an, dass foreach mit dem Array selbst arbeitete. 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 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 das manual schauen, 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 ... Dies scheint darauf foreach , 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? 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 das der Fall wäre, würde Testfall 1 für immer durchlaufen .

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 es begrüßen, wenn jemand es für mich ins Englische übersetzen könnte.

Mir scheint, dass foreach mit einer Kopie des Arrays arbeitet, 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 wirklich?
  • Gibt es Situationen, in denen die Verwendung von Funktionen, die den Array-Zeiger ( each() , reset() foreach ) während einer foreach anpassen, das Ergebnis der Schleife beeinflussen könnte?

Answers

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 bei 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 tatsächliche 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.

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. Dieser Verweis 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 PHP-Ternär-Operator: Schnell oder nicht?


Great question, because many developers, even experienced ones, are confused by the way PHP handles arrays in foreach loops. In the standard foreach loop, PHP makes a copy of the array that is used in the loop. The copy is discarded immediately after the loop finishes. This is transparent in the operation of a simple foreach loop. Beispielsweise:

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

This outputs:

apple
banana
coconut

So the copy is created but the developer doesn't notice, because the original array isn't referenced within the loop or after the loop finishes. However, when you attempt to modify the items in a loop, you find that they are unmodified when you finish:

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

print_r($set);

This outputs:

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

Any changes from the original can't be notices, actually there are no changes from the original, even though you clearly assigned a value to $item. This is because you are operating on $item as it appears in the copy of $set being worked on. You can override this by grabbing $item by reference, like so:

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

This outputs:

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

So it is evident and observable, when $item is operated on by-reference, the changes made to $item are made to the members of the original $set. Using $item by reference also prevents PHP from creating the array copy. To test this, first we'll show a quick script demonstrating the copy:

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

This outputs:

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

As it is shown in the example, PHP copied $set and used it to loop over, but when $set was used inside the loop, PHP added the variables to the original array, not the copied array. Basically, PHP is only using the copied array for the execution of the loop and the assignment of $item. Because of this, the loop above only executes 3 times, and each time it appends another value to the end of the original $set, leaving the original $set with 6 elements, but never entering an infinite loop.

However, what if we had used $item by reference, as I mentioned before? A single character added to the above test:

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

Results in an infinite loop. Note this actually is an infinite loop, you'll have to either kill the script yourself or wait for your OS to run out of memory. I added the following line to my script so PHP would run out of memory very quickly, I suggest you do the same if you're going to be running these infinite loop tests:

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

So in this previous example with the infinite loop, we see the reason why PHP was written to create a copy of the array to loop over. When a copy is created and used only by the structure of the loop construct itself, the array stays static throughout the execution of the loop, so you'll never run into issues.


Erklärung (Zitat von php.net ):

Die erste Form läuft über das durch array_expression angegebene Array. Bei jeder Iteration wird der Wert des aktuellen Elements $ value zugewiesen und der interne Array-Zeiger wird um eins erhöht (bei der nächsten Iteration sehen Sie also das nächste Element).

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, dass dies alles Folge von jedem Iterationsteil der Erklärung in der Dokumentation ist, was wahrscheinlich bedeutet, dass foreach alle Logik vor dem Aufruf des Codes in {} .

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

You will get:

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

Which means that array was modified, but since we modified it when the foreach already was at the last element of the array, it "decided" not to loop anymore, and even though we added new element, we added it "too late" and it was not looped through.

Detailed explanation can be read at How does PHP 'foreach' actually work? which explains the internals behind this behaviour.


As per the documentation provided by PHP manual.

On each iteration, the value of the current element is assigned to $v and the internal
array pointer is advanced by one (so on the next iteration, you'll be looking at the next element).

So as per your first example:

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

$array have only single element, so as per the foreach execution, 1 assign to $v and it don't have any other element to move pointer

But in your second example:

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

$array have two element, so now $array evaluate the zero indices and move the pointer by one. For first iteration of loop, added $array['baz']=3; as pass by reference.


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 sind 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 eine interne API verwendet wird, die im Wesentlichen nur die Iterator Schnittstelle auf der C-Ebene spiegelt.

Die Iteration von Arrays und einfachen Objekten ist wesentlich komplizierter. Zuallererst sollte angemerkt werden, dass "Arrays" in PHP wirklich geordnete Wörterbücher sind und nach dieser Reihenfolge durchlaufen werden (was dem Insertionsauftrag entspricht, solange Sie nicht etwas 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 foreach ($arr as &$v) iterieren, wird $arr in eine Referenz umgewandelt und Sie können sie während der Iteration ändern.
  • In PHP 5 gilt das auch dann, wenn Sie nach Wert iterieren, aber das Array war vorher eine Referenz: $ref =& $arr; foreach ($ref as $v) $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 mit Änderungen während der Iteration ist der Fall, wenn das Element, auf dem Sie gerade sind, 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. Für eine ausführlichere Einführung zu diesem Thema werden Sie belogen .

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 einen pro- 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

Der IAP ist ein sichtbares Merkmal eines Arrays (das durch die current Familie von Funktionen verfügbar gemacht wird), da solche Änderungen des IAP als Modifikationen unter der Semantik des Kopierens bei Schreiben zählen. 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 sich um eine Referenz handelt, sollten sich Änderungen daran ausbreiten, 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 wird $arr dupliziert, um zu verhindern, dass IAP-Änderungen an $arr 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 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);
}

Da foreach jedoch eine ganz besondere Schneeflocke ist, möchte man 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, bevor der Schleifenkörper ausgeführt wird. Dies bedeutet, dass, während der Schleifenkörper am Element $i , der IAP bereits am Element $i+1 . Dies ist der Grund, warum Codebeispiele, die während der Iteration eine Änderung zeigen, immer das nächste Element und nicht das aktuelle Element aufheben.

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 einigen Beispielen zuwenden.

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

  • In Testfällen beginnt 1 und 2 $array 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 nicht modifizierten Kopie von $array .

  • In Testfall 3 wird das Array erneut nicht dupliziert, daher wird foreach den IAP der Variablen $array ändern. Am Ende der Iteration ist der IAP NULL (was Iteration bedeutet), was each anzeigt, indem er false .

  • In den Testfällen 4 und 5 sind sowohl die each als auch die reset durch Referenzfunktionen ausgeführt. Das $array refcount=2 hat einen refcount=2 wenn es an sie übergeben wird, also muss es dupliziert werden. Daher wird foreach wieder an einem separaten Array arbeiten.

Beispiele: Auswirkungen von current in foreach

Eine gute Möglichkeit, die verschiedenen Duplikationsverhalten zu zeigen, besteht darin, das Verhalten der Funktion current() innerhalb einer foreach-Schleife 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 mit allen anderen Funktionen wie dem next die alle By-Ref sind, nett zu spielen. By-Reference Passing impliziert, dass das Array getrennt werden muss und somit $array und das foreach-array unterschiedlich sind. Der Grund, warum Sie 2 anstelle von 1 wird auch oben erwähnt: foreach rückt den Array-Zeiger vor , bevor der Benutzercode ausgeführt wird, 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 die Funktion by-ref current() wird. So arbeiten current() und foreach am selben Array. Aufgrund der Art und Weise, wie foreach den Mauszeiger bewegt, wird das Verhalten der einzelnen Personen jedoch immer noch foreach .

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 $array einem is_ref = 1 macht, wenn es durch Referenz iteriert wird, also 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 ist der Refcount des $array -Arrays 2, wenn die Schleife gestartet wird, also müssen wir die Duplizierung im Voraus durchführen. Somit werden $array und das von foreach verwendete Array 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, dass (1, 2) in der Ausgabe fehlt, weil Element 1 entfernt wurde. 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, werden die aktuelle IAP-Position und der Hash in einem 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 Konsequenz des HashPointer Backup- und Restore-Mechanismus ist, dass Änderungen am IAP durch reset() etc. normalerweise keinen Einfluss auf foreach haben. Der folgende Code wird beispielsweise so ausgeführt, als ob das reset() gar 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, während reset() temporär den IAP modifiziert, er auf das aktuelle foreach-Element nach dem Schleifenkörper zurückgesetzt wird. Um reset() zu erzwingen, um die Schleife zu beeinflussen, 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 gesund. Der eigentliche 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! Das bedeutet, dass wir mit einer sorgfältigen Auswahl der Array-Schlüssel dafür sorgen können, dass ein Element, das entfernt wurde, immer noch existiert, also direkt zu ihm 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' den gleichen Hashwert wie das entfernte Element 'EzFY' hat und der 'EzFY' den gleichen Speicherplatz erneut verwendet, 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 und eine Interaktion mit reset() usw. zu unterstützen.

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 das IAP nicht mehr verwenden wird. Die foreach-Schleife hat absolut keinen Einfluss auf die Ergebnisse von current() etc. und ihr eigenes Verhalten wird niemals durch Funktionen wie reset() usw. beeinflusst.

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 an den ursprünglichen Elementen arbeiten, ohne irgendwelche Ä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 (dh sie verhalten sich referenzähnlich, auch in by-value-Kontexten).

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 den IAP nicht mehr, sodass each() nicht von der Schleife betroffen ist. Es wird die gleiche Ausgabe vorher und nachher haben.

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

Der zweite Satz von Beispielen bezog sich auf das Verhalten von current() unter verschiedenen Referenz / Refcounting-Konfigurationen. Dies macht keinen Sinn mehr, da current() von der Schleife völlig unbeeinflusst bleibt und 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.


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

a) foreach arbeitet an der prospektierten Kopie des ursprünglichen Arrays. Dies bedeutet, dass foreach () SHARED-Datenspeicher haben wird, bis oder solange keine prospected copy für manual .

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

c) Der ursprüngliche DISTINCT SENTINEL VARIABLES und foreach () - Iterator wird DISTINCT SENTINEL VARIABLES haben, DISTINCT SENTINEL VARIABLES einen für das ursprüngliche Array und einen anderen für foreach; Sehen Sie den Testcode unten. SPL , Iterators und Array-Iterator .

Stapelüberlauffrage Wie kann sichergestellt werden, 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 jedes () und reset () SENTINEL-Variablen (for example, the current index variable) SENTINEL (for example, the current index variable) des foreach () - Iterators NICHT beeinflusst.

$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

Ich möchte nur sagen, dass ich ein bestimmtes Objekt mit variablen Attributen hatte (es war im Wesentlichen eine Zuordnung einer Tabelle und ich habe die Spalten in der Tabelle geändert, sodass die Attribute im Objekt, die die Tabelle widerspiegeln, ebenfalls variieren könnten

class obj {
    protected $fields = array('field1','field2');
    protected $field1 = array();
    protected $field2 = array();
    protected loadfields(){} 
    // This will load the $field1 and $field2 with rows of data for the column they describe
    protected function clearFields($num){
        foreach($fields as $field) {
            unset($this->$field[$num]); 
            // This did not work the line below worked
            unset($this->{$field}[$num]); // You have to resolve $field first using {}
        }
    }
}

Der Zweck von $ -Feldern bestand nur aus dem Grund, dass ich nicht überall im Code nachsehen muss, wenn sie geändert werden. Ich schaue nur den Anfang der Klasse an und ändere die Liste der Attribute und den Inhalt des $ -Feld- Arrays entsprechend neue Attribute.

Ich brauchte eine Weile, um das herauszufinden. Hoffe das kann jemandem helfen.





php loops foreach iteration php-internals