PHP Foreach Lulus dengan Referensi: Duplikasi Elemen Terakhir? (Bug?)

159

Saya baru saja memiliki perilaku yang sangat aneh dengan skrip php sederhana yang saya tulis. Saya mengurangi jumlah minimum yang diperlukan untuk membuat ulang bug:

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

?>

Output ini:

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

Apakah ini bug atau perilaku aneh yang seharusnya terjadi?

kerajaan
sumber
Lakukan berdasarkan nilai lagi, lihat apakah perubahan yang ketiga kalinya ...?
Shackrock
1
@Shackrock, sepertinya tidak berubah lagi dengan mengulangi loop berdasarkan nilai.
regality
1
Menariknya jika Anda mengubah loop kedua untuk menggunakan sesuatu selain $ item maka itu berfungsi seperti yang diharapkan.
Steve Claridge
9
selalu setel item di akhir loop body: foreach($x AS &$y){ ... unset($y); }- sebenarnya ada di php.net (tidak tahu di mana) karena itu adalah kesalahan yang banyak dibuat.
Rudie
2
kemungkinan rangkap dari PHP Pass dengan referensi di foreach
Felix Kling

Jawaban:

170

Setelah loop foreach pertama, $itemmasih referensi ke beberapa nilai yang juga digunakan oleh $arr[2]. Jadi setiap panggilan foreach di loop kedua, yang tidak memanggil dengan referensi, menggantikan nilai itu, dan dengan demikian $arr[2], dengan nilai baru.

Jadi loop 1, nilainya dan $arr[2]menjadi $arr[0], yaitu 'foo'.
Loop 2, nilai dan $arr[2]menjadi $arr[1], yang merupakan 'bar'.
Loop 3, nilai dan $arr[2]menjadi $arr[2], yang merupakan 'bar' (karena loop 2).

Nilai 'baz' sebenarnya hilang pada panggilan pertama dari loop foreach kedua.

Debugging Output

Untuk setiap iterasi dari loop, kami akan menggema nilai $itemserta mencetak array secara rekursif $arr.

Ketika loop pertama dijalankan, kita melihat output ini:

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

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

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

Di akhir perulangan, $itemmasih menunjuk ke tempat yang sama dengan $arr[2].

Ketika loop kedua dijalankan, kita melihat output ini:

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

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

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

Anda akan melihat bagaimana setiap array waktu memasukkan nilai baru $item, juga diperbarui $arr[3]dengan nilai yang sama, karena keduanya masih menunjuk ke lokasi yang sama. Ketika loop sampai ke nilai ketiga dari array, itu akan berisi nilai barkarena hanya diatur oleh iterasi sebelumnya dari loop itu.

Apakah ini bug?

Tidak. Ini adalah perilaku dari item yang dirujuk, dan bukan bug. Ini akan mirip dengan menjalankan sesuatu seperti:

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

Loop foreach tidak bersifat khusus di mana ia dapat mengabaikan item yang direferensikan. Ini hanya mengatur variabel itu ke nilai baru setiap kali seperti yang Anda lakukan di luar loop.

animuson
sumber
4
Saya memiliki sedikit koreksi yang dahsyat. $itembukan referensi ke $arr[2], nilai yang terkandung oleh $arr[2]adalah referensi ke nilai yang dirujuk oleh $item. Untuk mengilustrasikan perbedaannya, Anda juga dapat menghapus $arr[2], dan $itemtidak akan terpengaruh, dan menulis untuk $itemtidak akan mempengaruhinya.
Paul Biggar
2
Perilaku ini rumit untuk dipahami dan dapat menyebabkan masalah. Saya menyimpan ini sebagai salah satu favorit saya untuk menunjukkan kepada murid-murid saya mengapa mereka harus menghindari (selama mereka bisa) hal-hal "dengan referensi".
Olivier Pons
1
Mengapa tidak $itemkeluar dari ruang lingkup ketika loop foreach keluar? Ini sepertinya masalah penutupan?
jocull
6
@ jocull: DALAM PHP, foreach, untuk, sementara, dll tidak membuat ruang lingkup mereka sendiri.
animuson
1
@ jocull, PHP tidak memiliki (memblokir) variabel lokal. Salah satu alasan itu mengganggu saya.
Qtax
29

$itemadalah referensi ke $arr[2]dan sedang ditimpa oleh loop foreach kedua seperti yang ditunjukkan 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
sumber
3

Meskipun ini mungkin bukan bug resmi, menurut saya itu bug. Saya pikir masalahnya di sini adalah kita memiliki harapan untuk $itemkeluar dari ruang lingkup ketika loop keluar seperti dalam banyak bahasa pemrograman lainnya. Namun itu tampaknya tidak menjadi masalah ...

Kode ini ...

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

Memberikan output ...

one
two
three
three

Seperti yang sudah dikatakan orang lain, Anda menimpa variabel referensi $arr[2]dengan loop kedua Anda, tetapi itu hanya terjadi karena $itemtidak pernah keluar dari ruang lingkup. Apa yang kalian pikirkan ... bug?

jocull
sumber
4
1) Bukan bug. Sudah dipanggil dalam manual dan diberhentikan dalam sejumlah laporan bug sebagaimana dimaksud. 2) Tidak benar-benar menjawab pertanyaan ...
BoltClock
Itu menarik saya keluar bukan karena masalah ruang lingkup, saya mengharapkan $ item untuk tetap ada setelah foreach awal, tetapi saya tidak menyadari bahwa praeach MEMPERBARUI variabel bukannya MENGGANTI itu. misal sama dengan menjalankan unset ($ item) sebelum loop kedua. Perhatikan bahwa unset tidak menghapus nilai (dan dengan demikian elemen terakhir dalam array) hanya menghapus variabel.
Programster
Sayangnya, PHP tidak membuat ruang lingkup baru untuk loop atau {}blok secara umum. Beginilah cara kerjanya bahasa
Fabian Schmengler
0

Perilaku PHP yang benar bisa menjadi kesalahan PEMBERITAHUAN menurut pendapat saya. Jika variabel referensi dibuat dalam loop foreach digunakan di luar loop itu harus menyebabkan pemberitahuan. Sangat mudah jatuh dalam perilaku ini, sangat sulit dikenali ketika itu terjadi. Dan tidak ada pengembang yang akan membaca halaman dokumentasi foreach, itu tidak membantu.

Anda harus unset()referensi setelah loop Anda untuk menghindari masalah semacam ini. unset () pada referensi hanya akan menghapus referensi tanpa merusak data asli.

John
sumber
0

itu karena Anda menggunakan direktif ref (&). nilai terakhir akan diganti oleh loop kedua dan merusak array Anda. solusi paling sederhana adalah dengan menggunakan nama yang berbeda untuk loop kedua:

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

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