PHP Foreach Pass przez odniesienie: Kopiowanie ostatniego elementu? (Pluskwa?)

159

Po prostu miałem bardzo dziwne zachowanie z prostym skryptem php, który pisałem. Zredukowałem to do minimum niezbędnego do odtworzenia błędu:

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

?>

To daje:

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

Czy to błąd, czy jakieś naprawdę dziwne zachowanie, które powinno się wydarzyć?

królewskość
źródło
Zrób to ponownie według wartości, zobacz, czy zmieni się po raz trzeci ...?
Shackrock
1
@Shackrock, wydaje się, że nie zmienia się już z powtarzaniem pętli według wartości.
regality
1
Co ciekawe, jeśli zmienisz drugą pętlę, aby używała czegoś innego niż $ item, to działa zgodnie z oczekiwaniami.
Steve Claridge
9
zawsze odznacz element na końcu treści pętli: foreach($x AS &$y){ ... unset($y); }- tak naprawdę jest na php.net (nie wiem gdzie), ponieważ jest to dużo popełniony błąd.
Rudie
2
możliwy duplikat PHP Pass przez odniesienie w foreach
Felix Kling

Odpowiedzi:

170

Po pierwszej pętli foreach $itemnadal znajduje się odniesienie do jakiejś wartości, z której również korzysta $arr[2]. Zatem każde wywołanie foreach w drugiej pętli, które nie wywołuje przez odwołanie, zastępuje tę wartość, a tym samym $arr[2]nową wartością.

Więc pętla 1, wartość i $arr[2]get $arr[0], czyli „foo”.
Pętla 2, wartość i $arr[2]staje się $arr[1], czyli „bar”.
Loop 3, wartość i $arr[2]get $arr[2], czyli „bar” (z powodu pętli 2).

Wartość „baz” jest faktycznie tracona przy pierwszym wywołaniu drugiej pętli foreach.

Debugowanie danych wyjściowych

Dla każdej iteracji pętli będziemy powtarzać wartość, $itema także rekurencyjnie wypisywać tablicę $arr.

Po wykonaniu pierwszej pętli widzimy następujące dane wyjściowe:

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

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

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

Na końcu pętli $itemnadal wskazuje to samo miejsce co $arr[2].

Po wykonaniu drugiej pętli widzimy następujące dane wyjściowe:

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

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

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

Zauważysz, że za każdym razem, gdy tablica umieszcza nową wartość $item, jest również aktualizowana $arr[3]o tę samą wartość, ponieważ obie nadal wskazują to samo miejsce. Gdy pętla dotrze do trzeciej wartości tablicy, będzie zawierała wartość, barponieważ została ustawiona przez poprzednią iterację tej pętli.

Czy to błąd?

Nie. To jest zachowanie przywoływanego elementu, a nie błąd. Byłoby to podobne do uruchomienia czegoś takiego:

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

Pętla foreach nie ma specjalnego charakteru, w którym może ignorować elementy, do których się odwołuje. Po prostu ustawia tę zmienną na nową wartość za każdym razem, jakbyś był poza pętlą.

animuson
źródło
4
Mam lekką pedantyczną korektę. $itemnie jest odniesieniem do $arr[2], wartość zawarta w $arr[2]jest odniesieniem do wartości, do której odwołuje się $item. Aby zilustrować różnicę, możesz również wyłączyć ustawienie $arr[2]i $itemnie będzie to miało wpływu, a pisanie w celu $itemnie wpłynie na to.
Paul Biggar,
2
To zachowanie jest trudne do zrozumienia i może prowadzić do problemów. Uważam to za jedno z moich ulubionych, aby pokazać moim uczniom, dlaczego powinni unikać (tak długo, jak mogą) rzeczy „przez odniesienie”.
Olivier Pons
1
Dlaczego $itemnie wychodzi poza zakres, gdy kończy się pętla foreach? Wygląda na to, że problem z zamknięciem?
wesoły
6
@jocull: IN PHP, foreach, for, while itp. nie tworzą własnego zakresu.
animuson
1
@jocull, PHP nie ma (blokowych) zmiennych lokalnych. Jednym z powodów, dla których to mnie denerwuje.
Qtax
29

$itemjest odniesieniem do $arr[2]drugiej pętli foreach i jest przez nią nadpisywany, jak wskazał Animuson.

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
źródło
3

Chociaż oficjalnie może to nie być błąd, moim zdaniem tak jest. Myślę, że problem polega na tym, że oczekujemy, $itemże wyjdziemy poza zakres, gdy pętla zostanie zamknięta, tak jak ma to miejsce w wielu innych językach programowania. Jednak wydaje się, że tak nie jest ...

Ten kod ...

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

Daje wyjście ...

one
two
three
three

Jak już powiedzieli inni ludzie, nadpisujesz zmienną, do której się odwołujesz, $arr[2]drugą pętlą, ale dzieje się to tylko dlatego, że $itemnigdy nie wyszło poza zakres. Co wy myślicie ... błąd?

żartobliwy
źródło
4
1) To nie jest błąd. Jest to już opisane w instrukcji i odrzucone w wielu zgłoszeniach błędów, zgodnie z przeznaczeniem. 2) Tak naprawdę nie odpowiada na pytanie ...
BoltClock
Zaskoczyło mnie to nie z powodu problemu z zakresem, spodziewałem się, że $ item pozostanie w pobliżu po początkowym foreach, ale nie zdawałem sobie sprawy, że foreach AKTUALIZUJE zmienną zamiast ją ZASTĄPIĆ. np. to samo co uruchomienie unset ($ item) przed drugą pętlą. Zauważ, że nieustawienie nie czyści wartości (a tym samym ostatniego elementu tablicy), po prostu usuwa zmienną.
Programster
Niestety, PHP {}ogólnie nie tworzy nowego zakresu dla pętli lub bloków. Tak działa język
Fabian Schmengler
0

Moim zdaniem poprawne zachowanie PHP powinno być POWIADOMIENIEM. Jeśli zmienna, do której istnieje odwołanie, utworzona w pętli foreach jest używana poza pętlą, powinno to spowodować powiadomienie. Bardzo łatwo dać się nabrać na takie zachowanie, bardzo trudno je zauważyć, kiedy to się stało. Żaden programista nie przeczyta każdej strony dokumentacji, to nie jest pomoc.

Powinieneś unset()odwołać się po pętli, aby uniknąć tego rodzaju problemów. unset () w odwołaniu spowoduje po prostu usunięcie odniesienia bez szkody dla oryginalnych danych.

Jan
źródło
0

to dlatego, że używasz przez dyrektywę ref (&). ostatnia wartość zostanie zastąpiona przez drugą pętlę, co spowoduje uszkodzenie tablicy. najprostszym rozwiązaniem jest użycie innej nazwy dla drugiej pętli:

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

foreach ($arr as $anotherItem) { ... }
Amir Surnay
źródło