PHP Foreach Pass by Reference: Letztes Element duplizieren? (Fehler?)

159

Ich hatte gerade ein sehr seltsames Verhalten mit einem einfachen PHP-Skript, das ich schrieb. Ich habe es auf das Minimum reduziert, das notwendig ist, um den Fehler neu zu erstellen:

<?php

$arr = array("foo",
             "bar",
             "baz");

foreach ($arr as &$item) { /* do nothing by reference */ }
print_r($arr);

foreach ($arr as $item) { /* do nothing by value */ }
print_r($arr); // $arr has changed....why?

?>

Dies gibt aus:

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

Ist das ein Fehler oder ein wirklich seltsames Verhalten, das passieren soll?

Königlichkeit
quelle
Wiederholen Sie den Wert erneut, um festzustellen, ob sich das dritte Mal ändert ...?
Shackrock
1
@Shackrock, es scheint sich nicht mehr zu ändern, wenn Schleifen nach Wert wiederholt werden.
Königlichkeit
1
Interessanterweise funktioniert es wie erwartet, wenn Sie die zweite Schleife so ändern, dass etwas anderes als $ item verwendet wird.
Steve Claridge
9
Deaktivieren Sie das Element immer am Ende des Schleifenkörpers: foreach($x AS &$y){ ... unset($y); }- Es befindet sich tatsächlich auf php.net (weiß nicht wo), da es sich um einen häufig gemachten Fehler handelt.
Rudie
2
mögliches Duplikat von PHP Pass durch Bezugnahme in foreach
Felix Kling

Antworten:

170

Nach der ersten foreach-Schleife $itemwird immer noch auf einen Wert verwiesen, der auch von verwendet wird $arr[2]. Jeder foreach-Aufruf in der zweiten Schleife, der nicht als Referenz aufgerufen wird, ersetzt diesen Wert und damit $arr[2]den neuen Wert.

Also Schleife 1, den Wert und $arr[2]werden $arr[0], was 'foo' ist.
Schleife 2, der Wert und $arr[2]werden $arr[1], was 'bar' ist.
Schleife 3, der Wert und $arr[2]werden $arr[2], was 'bar' ist (wegen Schleife 2).

Der Wert 'baz' geht beim ersten Aufruf der zweiten foreach-Schleife tatsächlich verloren.

Debuggen der Ausgabe

Für jede Iteration der Schleife geben wir den Wert des $itemArrays wieder und drucken es rekursiv aus $arr.

Wenn die erste Schleife durchlaufen wird, sehen wir diese Ausgabe:

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

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

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

Zeigt am Ende der Schleife $itemimmer noch auf die gleiche Stelle wie $arr[2].

Wenn die zweite Schleife durchlaufen wird, sehen wir diese Ausgabe:

foo
Array ( [0] => foo [1] => bar [2] => foo )

bar
Array ( [0] => foo [1] => bar [2] => bar )

bar
Array ( [0] => foo [1] => bar [2] => bar )

Sie werden feststellen, dass jedes Mal, wenn ein Array einen neuen Wert einfügt $item, dieser ebenfalls $arr[3]mit demselben Wert aktualisiert wird, da beide immer noch auf denselben Speicherort verweisen. Wenn die Schleife den dritten Wert des Arrays erreicht, enthält sie den Wert, barda er gerade durch die vorherige Iteration dieser Schleife festgelegt wurde.

Ist es ein Fehler?

Nein. Dies ist das Verhalten eines referenzierten Elements und kein Fehler. Es wäre ähnlich wie beim Ausführen von:

for ($i = 0; $i < count($arr); $i++) { $item = $arr[$i]; }

Eine foreach-Schleife ist nichts Besonderes und kann referenzierte Elemente ignorieren. Sie setzen diese Variable einfach jedes Mal auf den neuen Wert, wie Sie es außerhalb einer Schleife tun würden.

animuson
quelle
4
Ich habe eine leichte pedantische Korrektur. $itemist kein Verweis auf $arr[2], der in enthaltene Wert $arr[2]ist ein Verweis auf den von $item. Um den Unterschied zu veranschaulichen, könnten Sie auch deaktivieren $arr[2]und $itemwären davon nicht betroffen, und das Schreiben an $itemwürde dies nicht beeinflussen.
Paul Biggar
2
Dieses Verhalten ist komplex zu verstehen und kann zu Problemen führen. Ich behalte dies als einen meiner Favoriten, um meinen Schülern zu zeigen, warum sie (so lange sie können) Dinge "durch Bezugnahme" vermeiden sollten.
Olivier Pons
1
Warum $itemwird der Gültigkeitsbereich nicht verlassen, wenn die foreach-Schleife beendet wird? Dies scheint ein Schließungsproblem zu sein?
Jocull
6
@jocull: IN PHP erstellen foreach, for, while usw. keinen eigenen Bereich.
Animuson
1
@jocull, PHP hat keine (blockierten) lokalen Variablen. Einer der Gründe, warum es mich nervt.
Qtax
29

$itemist ein Verweis auf $arr[2]und wird von der zweiten foreach-Schleife überschrieben, wie animuson hervorhob.

foreach ($arr as &$item) { /* do nothing by reference */ }
print_r($arr);

unset($item); // This will fix the issue.

foreach ($arr as $item) { /* do nothing by value */ }
print_r($arr); // $arr has changed....why?
Michael Leaney
quelle
3

Obwohl dies offiziell kein Fehler sein mag, ist es meiner Meinung nach so. Ich denke, das Problem hier ist, dass wir die Erwartung haben, dass wir den $itemBereich verlassen, wenn die Schleife verlassen wird, wie es in vielen anderen Programmiersprachen der Fall wäre. Dies scheint jedoch nicht der Fall zu sein ...

Dieser Code ...

$arr = array('one', 'two', 'three');
foreach($arr as $item){
    echo "$item\n";
}    
echo $item;

Gibt die Ausgabe ...

one
two
three
three

Wie andere bereits gesagt haben, überschreiben Sie die referenzierte Variable $arr[2]mit Ihrer zweiten Schleife, aber dies geschieht nur, weil $itemder Gültigkeitsbereich nie überschritten wurde . Was meint ihr ... Fehler?

scherzhaft
quelle
4
1) Kein Fehler. Es wurde bereits im Handbuch erwähnt und in einer Reihe von Fehlerberichten wie beabsichtigt verworfen. 2) Beantwortet die Frage nicht wirklich ...
BoltClock
Es hat mich nicht wegen des Scope-Problems überrascht. Ich habe erwartet, dass $ item nach dem ersten foreach erhalten bleibt, aber ich habe nicht bemerkt, dass foreach die Variable AKTUALISIERT, anstatt sie zu ersetzen. zB das gleiche wie das Ausführen von unset ($ item) vor der zweiten Schleife. Beachten Sie, dass das Nicht-Setzen den Wert (und damit das letzte Element im Array) nicht löscht, sondern lediglich die Variable entfernt.
Programster
Leider erstellt PHP {}im Allgemeinen keinen neuen Bereich für Schleifen oder Blöcke. So funktioniert die Sprache
Fabian Schmengler
0

Das korrekte Verhalten von PHP könnte meiner Meinung nach ein NOTICE-Fehler sein. Wenn eine in einer foreach-Schleife erstellte referenzierte Variable außerhalb der Schleife verwendet wird, sollte dies einen Hinweis verursachen. Es ist sehr leicht, auf dieses Verhalten hereinzufallen, es ist sehr schwer zu erkennen, wenn es passiert ist. Und kein Entwickler wird die foreach-Dokumentationsseite lesen, es ist keine Hilfe.

Sie sollten unset()die Referenz nach Ihrer Schleife verwenden, um diese Art von Problem zu vermeiden. unset () für eine Referenz entfernt nur die Referenz, ohne die Originaldaten zu beschädigen.

John
quelle
0

Das liegt daran, dass Sie die Ref-Direktive (&) verwenden. Der letzte Wert wird durch die zweite Schleife ersetzt und Ihr Array wird beschädigt. Die einfachste Lösung besteht darin, einen anderen Namen für die zweite Schleife zu verwenden:

foreach ($arr as &$item) { ... }

foreach ($arr as $anotherItem) { ... }
Amir Surnay
quelle