Bagaimana cara kerja PHP 'foreach'?

2018

Biarkan saya awali ini dengan mengatakan bahwa saya tahu apa foreachitu, apakah dan bagaimana menggunakannya. Pertanyaan ini menyangkut bagaimana cara kerjanya di bawah kap, dan saya tidak ingin ada jawaban di sepanjang baris "ini adalah bagaimana Anda mengulang array dengan foreach".


Untuk waktu yang lama saya menganggap itu foreachbekerja dengan array itu sendiri. Kemudian saya menemukan banyak referensi tentang fakta bahwa ia bekerja dengan salinan array, dan sejak itu saya menganggap ini sebagai akhir dari cerita. Tetapi saya baru saja berdiskusi tentang masalah ini, dan setelah sedikit percobaan menemukan bahwa ini sebenarnya tidak 100% benar.

Biarkan saya menunjukkan apa yang saya maksud. Untuk kasus uji berikut, kami akan bekerja dengan array berikut:

$array = array(1, 2, 3, 4, 5);

Uji kasus 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 */

Ini jelas menunjukkan bahwa kami tidak bekerja secara langsung dengan array sumber - jika tidak, loop akan terus selamanya, karena kami terus-menerus mendorong item ke array selama loop. Tetapi untuk memastikan hal ini terjadi:

Uji kasus 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 */

Ini mendukung kesimpulan awal kami, kami bekerja dengan salinan array sumber selama loop, jika tidak kita akan melihat nilai yang dimodifikasi selama loop. Tapi...

Jika kita melihat di manual , kita menemukan pernyataan ini:

Ketika foreach pertama kali mulai dieksekusi, pointer array internal secara otomatis direset ke elemen pertama array.

Benar ... ini sepertinya menyarankan bahwa foreachbergantung pada pointer array dari array sumber. Tapi kami baru saja membuktikan bahwa kami tidak bekerja dengan array sumber , kan? Ya, tidak sepenuhnya.

Uji kasus 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)
*/

Jadi, terlepas dari kenyataan bahwa kami tidak bekerja secara langsung dengan array sumber, kami bekerja secara langsung dengan pointer array sumber - fakta bahwa pointer berada di ujung array di akhir loop menunjukkan ini. Kecuali ini tidak mungkin benar - jika benar, maka test case 1 akan berulang selamanya.

Manual PHP juga menyatakan:

Karena foreach bergantung pada pointer array internal mengubahnya dalam loop dapat menyebabkan perilaku yang tidak terduga.

Baiklah, mari kita cari tahu apa itu "perilaku tak terduga" (secara teknis, perilaku apa pun tidak terduga karena saya tidak lagi tahu apa yang diharapkan).

Uji kasus 4 :

foreach ($array as $key => $item) {
  echo "$item\n";
  each($array);
}

/* Output: 1 2 3 4 5 */

Uji kasus 5 :

foreach ($array as $key => $item) {
  echo "$item\n";
  reset($array);
}

/* Output: 1 2 3 4 5 */

... tidak ada yang tak terduga di sana, bahkan tampaknya mendukung teori "salinan sumber".


Pertanyaan

Apa yang terjadi disini? C-fu saya tidak cukup baik bagi saya untuk dapat mengambil kesimpulan yang tepat hanya dengan melihat kode sumber PHP, saya akan sangat menghargai jika seseorang dapat menerjemahkannya ke dalam Bahasa Inggris untuk saya.

Sepertinya saya yang foreachbekerja dengan salinan array, tetapi menetapkan pointer array dari array sumber ke akhir array setelah loop.

  • Apakah ini benar dan keseluruhan cerita?
  • Jika tidak, apa yang sebenarnya dilakukannya?
  • Apakah ada situasi di mana menggunakan fungsi yang mengatur pointer array ( each(), reset()et al.) Selama a foreachdapat mempengaruhi hasil loop?
DaveRandom
sumber
5
@DaveRandom Ada tag php-internal yang mungkin harus disertakan , tetapi saya akan menyerahkannya kepada Anda untuk memutuskan yang mana dari 5 tag lainnya yang akan diganti.
Michael Berkowski
5
terlihat seperti SAPI, tanpa pegangan hapus
zb '
149
Awalnya saya pikir »ya ampun, pertanyaan pemula lainnya. Baca dokumen ... hm, perilaku yang tidak jelas «. Kemudian saya membaca pertanyaan yang lengkap, dan saya harus mengatakan: Saya menyukainya. Anda telah berusaha keras di dalamnya dan menulis semua testcases. ps. Apakah testcase 4 dan 5 sama?
knittl
21
Hanya memikirkan mengapa masuk akal bahwa pointer array tersentuh: PHP perlu mengatur ulang dan memindahkan pointer array internal array asli bersama dengan salinan, karena pengguna dapat meminta referensi ke nilai saat ini ( foreach ($array as &$value)) - PHP perlu mengetahui posisi saat ini di array asli meskipun sebenarnya iterating di atas salinan.
Niko
4
@Sean: IMHO, dokumentasi PHP benar-benar sangat buruk dalam menggambarkan nuansa fitur bahasa inti. Tapi itu mungkin, karena begitu banyak kasus khusus ad-hoc dimasukkan ke dalam bahasa ...
Oliver Charlesworth

Jawaban:

1660

foreach mendukung iterasi dalam tiga jenis nilai:

Berikut ini, saya akan mencoba menjelaskan dengan tepat bagaimana iterasi bekerja dalam berbagai kasus. Sejauh ini kasus yang paling sederhana adalah Traversableobjek, karena ini foreachpada dasarnya hanya gula sintaks untuk kode di sepanjang baris ini:

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();
    /* ... */
}

Untuk kelas internal, panggilan metode aktual dihindari dengan menggunakan API internal yang pada dasarnya hanya mencerminkan Iteratorantarmuka pada level C.

Iterasi array dan objek polos secara signifikan lebih rumit. Pertama-tama, harus dicatat bahwa dalam PHP "array" adalah kamus yang benar-benar teratur dan mereka akan dilintasi menurut urutan ini (yang cocok dengan urutan penyisipan selama Anda tidak menggunakan sesuatu seperti sort). Ini bertentangan dengan pengulangan dengan urutan alami kunci (bagaimana daftar dalam bahasa lain sering bekerja) atau tidak memiliki urutan yang jelas sama sekali (bagaimana kamus dalam bahasa lain sering bekerja).

Hal yang sama juga berlaku untuk objek, karena properti objek dapat dilihat sebagai kamus lain (dipesan) nama properti pemetaan dengan nilai-nilai mereka, ditambah beberapa penanganan visibilitas. Dalam sebagian besar kasus, properti objek sebenarnya tidak disimpan dengan cara yang agak tidak efisien ini. Namun, jika Anda mulai mengulangi objek, representasi paket yang biasanya digunakan akan dikonversi ke kamus nyata. Pada titik itu, iterasi objek polos menjadi sangat mirip dengan iterasi array (itulah sebabnya saya tidak banyak membahas iterasi objek polos di sini).

Sejauh ini bagus. Mengurai kamus tidak terlalu sulit, bukan? Masalah dimulai ketika Anda menyadari bahwa array / objek dapat berubah selama iterasi. Ada beberapa cara ini bisa terjadi:

  • Jika Anda beralih menggunakan referensi foreach ($arr as &$v)maka $arrdiubah menjadi referensi dan Anda dapat mengubahnya selama iterasi.
  • Dalam PHP 5 hal yang sama berlaku bahkan jika Anda mengulanginya berdasarkan nilai, tetapi array adalah referensi sebelumnya: $ref =& $arr; foreach ($ref as $v)
  • Objek memiliki by-pass semantik, yang untuk sebagian besar tujuan praktis berarti bahwa mereka berperilaku seperti referensi. Jadi objek selalu dapat diubah selama iterasi.

Masalah dengan mengizinkan modifikasi selama iterasi adalah kasus di mana elemen Anda saat ini dihapus. Katakanlah Anda menggunakan pointer untuk melacak elemen larik Anda saat ini. Jika elemen ini sekarang dibebaskan, Anda dibiarkan dengan pointer menggantung (biasanya menghasilkan segfault).

Ada berbagai cara untuk menyelesaikan masalah ini. PHP 5 dan PHP 7 berbeda secara signifikan dalam hal ini dan saya akan menjelaskan kedua perilaku berikut ini. Kesimpulannya adalah bahwa pendekatan PHP 5 agak bodoh dan mengarah ke semua jenis masalah tepi-aneh, sementara pendekatan PHP 7 yang lebih terlibat menghasilkan perilaku yang lebih dapat diprediksi dan konsisten.

Sebagai pendahuluan terakhir, harus dicatat bahwa PHP menggunakan penghitungan referensi dan copy-on-write untuk mengelola memori. Ini berarti bahwa jika Anda "menyalin" suatu nilai, Anda sebenarnya hanya menggunakan kembali nilai yang lama dan menambah jumlah referensi (refcount). Hanya sekali Anda melakukan beberapa jenis modifikasi, salinan asli (disebut "duplikasi") akan dilakukan. Lihat Anda dibohongi untuk pengantar yang lebih luas tentang topik ini.

PHP 5

Pointer array internal dan HashPointer

Array di PHP 5 memiliki satu "internal array pointer" (IAP), yang mendukung modifikasi: Setiap kali elemen dihapus, akan ada pemeriksaan apakah IAP menunjuk ke elemen ini. Jika ya, alih-alih maju ke elemen berikutnya.

Meskipun foreachmemanfaatkan IAP, ada komplikasi tambahan: Hanya ada satu IAP, tetapi satu array dapat menjadi bagian dari banyak foreachloop:

// 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) {
        // ...
    }
}

Untuk mendukung dua loop simultan dengan hanya satu pointer array internal, foreachlakukan shenanigans berikut: Sebelum loop body dieksekusi, foreachakan membuat cadangan pointer ke elemen saat ini dan hash-nya menjadi per-foreach HashPointer. Setelah loop body berjalan, IAP akan diatur kembali ke elemen ini jika masih ada. Namun jika elemen telah dihapus, kami hanya akan menggunakan di mana pun IAP saat ini berada. Skema ini sebagian besar-agak-semacam bekerja, tetapi ada banyak perilaku aneh yang bisa Anda dapatkan darinya, beberapa di antaranya akan saya tunjukkan di bawah ini.

Duplikasi array

IAP adalah fitur yang terlihat dari sebuah array (diekspos melalui currentkeluarga fungsi), karena itu perubahan pada jumlah IAP sebagai modifikasi di bawah semantik copy-on-write. Sayangnya, ini berarti bahwa foreachdalam banyak kasus dipaksa untuk menduplikasi array yang sudah di-iterating. Kondisi yang tepat adalah:

  1. Array bukan referensi (is_ref = 0). Jika ini adalah referensi, maka perubahan yang seharusnya diperbanyak, sehingga tidak boleh digandakan.
  2. Array memiliki refcount> 1. Jika refcount1, maka array tidak dibagi dan kita bebas untuk memodifikasinya secara langsung.

Jika array tidak diduplikasi (is_ref = 0, refcount = 1), maka hanya arraynya yang refcountakan bertambah (*). Selain itu, jika foreachdengan referensi digunakan, maka array (berpotensi diduplikasi) akan diubah menjadi referensi.

Pertimbangkan kode ini sebagai contoh di mana duplikasi terjadi:

function iterate($arr) {
    foreach ($arr as $v) {}
}

$outerArr = [0, 1, 2, 3, 4];
iterate($outerArr);

Di sini, $arrakan digandakan untuk mencegah perubahan IAP $arrdari bocor ke $outerArr. Dalam hal kondisi di atas, array bukan referensi (is_ref = 0) dan digunakan di dua tempat (refcount = 2). Persyaratan ini sangat disayangkan dan merupakan artefak dari implementasi suboptimal (tidak ada masalah modifikasi selama iterasi di sini, jadi kita tidak benar-benar perlu menggunakan IAP sejak awal).

(*) Menambah refcountsini terdengar tidak berbahaya, tetapi melanggar semantik copy-on-write (COW): Ini berarti bahwa kita akan memodifikasi IAP dari array refcount = 2, sementara COW menentukan bahwa modifikasi hanya dapat dilakukan pada refcount = 1 nilai. Pelanggaran ini menghasilkan perubahan perilaku yang terlihat oleh pengguna (sementara SAP biasanya transparan) karena perubahan IAP pada array yang diulang akan dapat diamati - tetapi hanya sampai modifikasi non-IAP pertama pada array. Alih-alih, tiga opsi "valid" akan menjadi a) untuk selalu menduplikasi, b) tidak menambah refcountdan dengan demikian memungkinkan array iterated untuk diubah secara sewenang-wenang dalam loop atau c) tidak menggunakan IAP sama sekali (PHP sama sekali 7 solusi).

Urutan kenaikan posisi

Ada satu detail implementasi terakhir yang harus Anda perhatikan untuk memahami contoh kode dengan benar di bawah ini. Cara "normal" untuk perulangan melalui beberapa struktur data akan terlihat seperti ini di pseudocode:

reset(arr);
while (get_current_data(arr, &data) == SUCCESS) {
    code();
    move_forward(arr);
}

Namun foreach, karena kepingan salju yang agak istimewa, memilih untuk melakukan hal-hal yang sedikit berbeda:

reset(arr);
while (get_current_data(arr, &data) == SUCCESS) {
    move_forward(arr);
    code();
}

Yaitu, pointer array sudah bergerak maju sebelum loop body berjalan. Ini berarti bahwa sementara badan loop bekerja pada elemen $i, IAP sudah ada di elemen $i+1. Ini adalah alasan mengapa sampel kode yang menunjukkan modifikasi selama iterasi akan selalu unsetmenjadi elemen berikutnya , bukan yang sekarang.

Contoh: Kasing uji Anda

Tiga aspek yang dijelaskan di atas akan memberi Anda kesan yang lengkap tentang keanehan foreachimplementasi dan kami dapat melanjutkan untuk membahas beberapa contoh.

Perilaku kasus pengujian Anda mudah dijelaskan pada titik ini:

  • Dalam kasus uji 1 dan 2 $arraydimulai dengan refcount = 1, jadi itu tidak akan diduplikasi oleh foreach: Hanya yang refcountbertambah. Ketika badan loop selanjutnya memodifikasi array (yang memiliki refcount = 2 pada saat itu), duplikasi akan terjadi pada titik itu. Foreach akan terus mengerjakan salinan $array.

  • Dalam test case 3, sekali lagi array tidak diduplikasi, sehingga foreachakan memodifikasi IAP dari $arrayvariabel. Pada akhir iterasi, IAP adalah NULL (artinya iterasi telah dilakukan), yang eachmenunjukkan dengan mengembalikan false.

  • Dalam kasus uji 4 dan 5 keduanya eachdan resetmerupakan fungsi referensi. The $arraymemiliki refcount=2ketika diberikan kepada mereka, sehingga harus digandakan. Dengan demikian foreachakan bekerja pada array yang terpisah lagi.

Contoh: Efek currentineach

Cara yang baik untuk menunjukkan berbagai perilaku duplikasi adalah dengan mengamati perilaku current()fungsi di dalam foreachloop. Pertimbangkan contoh ini:

foreach ($array as $val) {
    var_dump(current($array));
}
/* Output: 2 2 2 2 2 */

Di sini Anda harus tahu bahwa itu current()adalah fungsi by-ref (sebenarnya: prefer-ref), meskipun itu tidak mengubah array. Itu harus untuk bermain bagus dengan semua fungsi lain seperti nextyang semuanya oleh-ref. Pass-reference passing menyiratkan bahwa array harus dipisahkan dan dengan demikian $arraydan foreach-arrayakan berbeda. Alasan Anda mendapatkan 2alih-alih 1juga disebutkan di atas: foreachmemajukan pointer array sebelum menjalankan kode pengguna, bukan setelahnya. Jadi meskipun kode berada di elemen pertama, foreachsudah maju pointer ke yang kedua.

Sekarang mari kita coba modifikasi kecil:

$ref = &$array;
foreach ($array as $val) {
    var_dump(current($array));
}
/* Output: 2 3 4 5 false */

Di sini kita memiliki case is_ref = 1, sehingga array tidak disalin (seperti di atas). Tapi sekarang itu adalah referensi, array tidak lagi harus diduplikasi ketika melewati fungsi by-ref current(). Jadi current()dan foreachbekerja pada array yang sama. Anda masih melihat perilaku off-by-one, karena cara foreachmemajukan pointer.

Anda mendapatkan perilaku yang sama saat melakukan by-ref iteration:

foreach ($array as &$val) {
    var_dump(current($array));
}
/* Output: 2 3 4 5 false */

Di sini bagian yang penting adalah bahwa foreach akan membuat $arrayis_ref = 1 ketika iterated oleh referensi, jadi pada dasarnya Anda memiliki situasi yang sama seperti di atas.

Variasi kecil lainnya, kali ini kami akan menetapkan array ke variabel lain:

$foo = $array;
foreach ($array as $val) {
    var_dump(current($array));
}
/* Output: 1 1 1 1 1 */

Di sini refcount dari $arrayadalah 2 ketika loop dimulai, jadi untuk sekali ini kita benar-benar harus melakukan duplikasi dimuka. Dengan demikian $arraydan array yang digunakan oleh foreach akan sepenuhnya terpisah dari permulaan. Itu sebabnya Anda mendapatkan posisi IAP di mana pun sebelum loop (dalam hal ini di posisi pertama).

Contoh: Modifikasi selama iterasi

Mencoba untuk memperhitungkan modifikasi selama iterasi adalah tempat semua masalah kami berasal, sehingga berfungsi untuk mempertimbangkan beberapa contoh untuk kasus ini.

Pertimbangkan loop bersarang ini di atas array yang sama (di mana by-ref iteration digunakan untuk memastikan itu benar-benar sama):

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)

Bagian yang diharapkan di sini adalah yang (1, 2)hilang dari output karena elemen 1telah dihapus. Apa yang mungkin tidak terduga adalah bahwa loop luar berhenti setelah elemen pertama. Mengapa demikian?

Alasan di balik ini adalah hack nested-loop yang dijelaskan di atas: Sebelum loop body berjalan, posisi IAP dan hash saat ini dicadangkan menjadi a HashPointer. Setelah loop body akan dikembalikan, tetapi hanya jika elemen masih ada, jika tidak posisi IAP saat ini (apa pun itu) digunakan sebagai gantinya. Dalam contoh di atas, inilah yang sebenarnya terjadi: Elemen saat ini dari loop luar telah dihapus, sehingga akan menggunakan IAP, yang telah ditandai sebagai selesai oleh loop dalam!

Konsekuensi lain dari HashPointermekanisme backup + restore adalah bahwa perubahan pada IAP melalui reset()dll. Biasanya tidak berdampak foreach. Misalnya, kode berikut dijalankan seolah-olah reset()tidak ada sama sekali:

$array = [1, 2, 3, 4, 5];
foreach ($array as &$value) {
    var_dump($value);
    reset($array);
}
// output: 1, 2, 3, 4, 5

Alasannya adalah bahwa, reset()sementara memodifikasi sementara IAP, itu akan dikembalikan ke elemen foreach saat ini setelah tubuh loop. Untuk memaksa reset()membuat efek pada loop, Anda harus menghapus elemen saat ini, sehingga mekanisme backup / restore gagal:

$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

Tapi, contoh-contoh itu masih waras. Kegembiraan yang sebenarnya dimulai jika Anda ingat bahwa HashPointerpengembalian menggunakan pointer ke elemen dan hash untuk menentukan apakah itu masih ada. Tetapi: Hash memiliki benturan, dan pointer dapat digunakan kembali! Ini berarti bahwa, dengan pilihan kunci array yang hati-hati, kita dapat foreachmeyakini bahwa elemen yang telah dihapus masih ada, sehingga akan langsung melompat ke sana. Sebuah contoh:

$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

Di sini kita biasanya mengharapkan output 1, 1, 3, 4sesuai dengan aturan sebelumnya. Bagaimana yang terjadi adalah yang 'FYFY'memiliki hash yang sama dengan elemen yang dihapus 'EzFY', dan pengalokasi terjadi untuk menggunakan kembali lokasi memori yang sama untuk menyimpan elemen. Jadi foreach akhirnya langsung melompat ke elemen yang baru dimasukkan, sehingga memotong pendek loop.

Mengganti entitas iterasi selama loop

Satu kasus aneh terakhir yang ingin saya sebutkan, adalah bahwa PHP memungkinkan Anda untuk mengganti entitas iterated selama loop. Jadi Anda bisa mulai iterasi pada satu array dan kemudian menggantinya dengan array lain di tengah jalan. Atau mulai iterasi pada array lalu ganti dengan objek:

$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 */

Seperti yang Anda lihat dalam hal ini PHP hanya akan mulai mengulangi entitas lain dari awal setelah substitusi terjadi.

PHP 7

Iterator yang mudah pecah

Jika Anda masih ingat, masalah utama dengan iterasi array adalah bagaimana menangani penghapusan elemen iterasi-tengah. PHP 5 menggunakan pointer array internal tunggal (IAP) untuk tujuan ini, yang agak suboptimal, karena satu pointer array harus diregangkan untuk mendukung beberapa loop foreach simultan dan interaksi dengan reset()dll di atas itu.

PHP 7 menggunakan pendekatan yang berbeda, yaitu, mendukung pembuatan sejumlah iterator eksternal, hashtable yang sewenang-wenang. Iterator ini harus didaftarkan dalam array, dari titik mana mereka memiliki semantik yang sama dengan IAP: Jika elemen array dihapus, semua iterator hashtable yang menunjuk ke elemen itu akan maju ke elemen berikutnya.

Ini berarti bahwa foreachtidak akan lagi menggunakan IAP sama sekali . The foreachLoop akan benar-benar tidak berpengaruh pada hasil current()dll dan perilaku sendiri tidak akan pernah dipengaruhi oleh fungsi seperti reset()dll

Duplikasi array

Perubahan penting lainnya antara PHP 5 dan PHP 7 terkait dengan duplikasi array. Sekarang IAP tidak lagi digunakan, iterasi array nilai-hanya akan melakukan refcountpeningkatan (bukan duplikasi array) dalam semua kasus. Jika array diubah selama foreachloop, pada saat itu duplikasi akan terjadi (sesuai dengan copy-on-write) dan foreachakan tetap bekerja pada array yang lama.

Dalam kebanyakan kasus, perubahan ini transparan dan tidak memiliki efek selain kinerja yang lebih baik. Namun, ada satu kesempatan di mana ia menghasilkan perilaku yang berbeda, yaitu kasus di mana array adalah referensi sebelumnya:

$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 */

Sebelumnya oleh-nilai iterasi array referensi adalah kasus khusus. Dalam hal ini, tidak ada duplikasi yang terjadi, jadi semua modifikasi array selama iterasi akan direfleksikan oleh loop. Dalam PHP 7 kasus khusus ini hilang: iterasi nilai-nilai dari array akan selalu bekerja pada elemen asli, mengabaikan modifikasi apa pun selama loop.

Ini, tentu saja, tidak berlaku untuk iterasi referensi. Jika Anda mengulangi dengan referensi semua modifikasi akan tercermin oleh loop. Menariknya, hal yang sama berlaku untuk iterasi nilai-rata objek polos:

$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 */

Hal ini mencerminkan semantik objek yang ditangani sendiri (yaitu mereka berperilaku seperti referensi bahkan dalam konteks oleh-nilai).

Contohnya

Mari kita perhatikan beberapa contoh, dimulai dengan test case Anda:

  • Kasing uji 1 dan 2 mempertahankan output yang sama: Iterasi array nilai-selalu bekerja pada elemen asli. (Dalam hal ini, genap refcountingdan perilaku duplikasi persis sama antara PHP 5 dan PHP 7).

  • Perubahan test case 3: Foreachtidak lagi menggunakan IAP, jadi each()tidak terpengaruh oleh loop. Ini akan memiliki output yang sama sebelum dan sesudah.

  • Kasing uji 4 dan 5 tetap sama: each()dan reset()akan menduplikasi array sebelum mengubah IAP, sementara foreachmasih menggunakan array asli. (Bukan berarti perubahan IAP akan menjadi masalah, bahkan jika array dibagikan.)

Rangkaian contoh kedua terkait dengan perilaku di current()bawah reference/refcountingkonfigurasi yang berbeda . Ini tidak lagi masuk akal, karena current()sama sekali tidak terpengaruh oleh loop, sehingga nilai pengembaliannya selalu tetap sama.

Namun, kami mendapatkan beberapa perubahan menarik ketika mempertimbangkan modifikasi selama iterasi. Saya harap Anda akan menemukan perilaku baru yang lebih waras. Contoh pertama:

$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) 

Seperti yang Anda lihat, loop luar tidak lagi dibatalkan setelah iterasi pertama. Alasannya adalah bahwa kedua loop sekarang memiliki iterator hashtable yang sepenuhnya terpisah, dan tidak ada lagi kontaminasi silang dari kedua loop melalui IAP bersama.

Kasing tepi aneh lain yang diperbaiki sekarang, adalah efek aneh yang Anda dapatkan ketika Anda menghapus dan menambahkan elemen yang memiliki hash yang sama:

$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

Sebelumnya mekanisme pemulihan HashPointer melompat tepat ke elemen baru karena "tampak" seperti itu sama dengan elemen yang dihapus (karena bertabrakan hash dan pointer). Karena kita tidak lagi mengandalkan hash elemen untuk apa pun, ini tidak lagi menjadi masalah.

NikiC
sumber
4
@Baba Tidak. Menularkannya ke fungsi sama dengan melakukan $foo = $arraysebelum loop;)
NikiC
32
Bagi Anda yang tidak tahu apa itu zval, silakan merujuk ke blog
shu zOMG chen
1
Koreksi kecil: apa yang Anda sebut Bucket bukan apa yang biasanya disebut Bucket dalam hashtable. Biasanya Bucket adalah sekumpulan entri dengan hash% size yang sama. Anda tampaknya menggunakannya untuk apa yang biasanya disebut entri. Daftar tertaut bukan pada bucket, tetapi pada entri.
unbeli
12
@ unbeli Saya menggunakan terminologi yang digunakan secara internal oleh PHP. The Buckets adalah bagian dari daftar ganda terkait untuk tabrakan hash dan juga bagian dari daftar ganda terkait untuk pesanan;)
NikiC
4
Jawaban yang bagus. Saya pikir Anda maksud iterate($outerArr);dan tidak di iterate($arr);suatu tempat.
niahoo
116

Dalam contoh 3 Anda tidak mengubah array. Dalam semua contoh lain Anda memodifikasi konten atau pointer array internal. Ini penting ketika menyangkut array PHP karena semantik dari operator penugasan.

Operator penugasan untuk array dalam PHP bekerja lebih seperti klon malas. Menetapkan satu variabel ke yang lain yang berisi array akan mengkloning array, tidak seperti kebanyakan bahasa. Namun, kloning yang sebenarnya tidak akan dilakukan kecuali jika diperlukan. Ini berarti bahwa klon akan terjadi hanya ketika salah satu variabel dimodifikasi (copy-on-write).

Berikut ini sebuah contoh:

$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.

Kembali ke kasus pengujian Anda, Anda dapat dengan mudah membayangkan bahwa foreachmenciptakan semacam iterator dengan referensi ke array. Referensi ini berfungsi persis seperti variabel $bdalam contoh saya. Namun, iterator bersama dengan referensi hanya hidup selama loop dan kemudian, keduanya dibuang. Sekarang Anda dapat melihat bahwa, dalam semua kasus kecuali 3, array dimodifikasi selama loop, sementara referensi tambahan ini masih hidup. Ini memicu klon, dan itu menjelaskan apa yang terjadi di sini!

Berikut ini adalah artikel yang bagus untuk efek samping lain dari perilaku copy-on-write ini: Operator Ternary PHP: Cepat atau tidak?

linepogl
sumber
tampaknya benar Anda, saya membuat beberapa contoh yang menunjukkan bahwa: codepad.org/OCjtvu8r satu perbedaan dari contoh Anda - tidak menyalin jika Anda mengubah nilai, hanya jika mengubah kunci.
zb '
Ini memang menjelaskan semua perilaku yang ditunjukkan di atas, dan itu dapat diilustrasikan dengan baik dengan memanggil each()pada akhir test case pertama, di mana kita melihat bahwa pointer array dari array asli menunjuk ke elemen kedua, karena array tersebut dimodifikasi selama iterasi pertama. Ini juga tampaknya menunjukkan bahwa foreachmemindahkan pointer array pada sebelum mengeksekusi blok kode loop, yang saya tidak harapkan - saya akan berpikir itu akan melakukan ini di akhir. Banyak terima kasih, ini jelas bagi saya.
DaveRandom
49

Beberapa hal yang perlu diperhatikan ketika bekerja dengan foreach():

a) foreachbekerja pada salinan prospektif dari array asli. Ini berarti foreach()akan memiliki penyimpanan data SHARED sampai atau kecuali jika prospected copytidak dibuat untuk setiap Catatan / komentar Pengguna .

b) Apa yang memicu salinan prospektif ? Salinan prospektif dibuat berdasarkan kebijakan copy-on-write, yaitu, setiap kali array yang diteruskan foreach()diubah, klon dari array asli dibuat.

c) Array asli dan foreach()iterator akan memiliki DISTINCT SENTINEL VARIABLES, yaitu, satu untuk array asli dan lainnya untuk foreach; lihat kode tes di bawah ini. SPL , Iterators , dan Array Iterator .

Pertanyaan Stack Overflow Bagaimana memastikan nilai diatur ulang dalam loop 'foreach' di PHP? membahas kasus-kasus (3,4,5) dari pertanyaan Anda.

Contoh berikut menunjukkan bahwa setiap () dan reset () TIDAK mempengaruhi SENTINELvariabel (for example, the current index variable)dari foreach()iterator.

$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/>";

Keluaran:

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
sakhunzai
sumber
2
Jawaban Anda kurang tepat. foreachberoperasi pada salinan potensial array, tetapi tidak membuat salinan yang sebenarnya kecuali diperlukan.
linepogl
apakah Anda ingin menunjukkan bagaimana dan kapan salinan potensial dibuat melalui kode? Kode saya menunjukkan bahwa foreachmenyalin array 100% dari waktu. Saya ingin sekali tahu. Terima kasih atas komentar Anda
sakhunzai
Menyalin array membutuhkan biaya banyak. Coba hitung waktu yang dibutuhkan untuk beralih array dengan 100000 elemen menggunakan salah satu foratau foreach. Anda tidak akan melihat perbedaan yang signifikan di antara keduanya, karena salinan yang sebenarnya tidak terjadi.
linepogl
Kemudian saya akan berasumsi bahwa ada SHARED data storagedicadangkan sampai atau kecuali copy-on-write, tetapi (dari potongan kode saya) jelas bahwa akan selalu ada DUA set SENTINEL variablessatu untuk original arraydan untuk lainnya foreach. Terima kasih itu masuk akal
sakhunzai
1
ya itu salinan "prospek" yaitu salinan "potensial". Tidak terlindungi seperti yang Anda sarankan
sakhunzai
33

CATATAN UNTUK PHP 7

Untuk memperbarui jawaban ini karena telah mendapatkan popularitas: Jawaban ini tidak lagi berlaku pada PHP 7. Seperti yang dijelaskan dalam " Perubahan ke belakang yang tidak kompatibel ", dalam PHP 7, masing-masing bekerja pada salinan array, sehingga setiap perubahan pada array itu sendiri tidak tercermin pada foreach loop. Lebih detail di tautan.

Penjelasan (kutipan dari php.net ):

Bentuk pertama loop atas array yang diberikan oleh array_expression. Pada setiap iterasi, nilai elemen saat ini ditetapkan ke nilai $ dan pointer array internal dikemukakan oleh satu (jadi pada iterasi berikutnya, Anda akan melihat elemen berikutnya).

Jadi, dalam contoh pertama Anda, Anda hanya memiliki satu elemen dalam array, dan ketika pointer dipindahkan, elemen berikutnya tidak ada, jadi setelah Anda menambahkan elemen baru untuk setiap ujung karena sudah "memutuskan" bahwa itu sebagai elemen terakhir.

Dalam contoh kedua Anda, Anda mulai dengan dua elemen, dan foreach loop bukan pada elemen terakhir sehingga mengevaluasi array pada iterasi berikutnya dan dengan demikian menyadari bahwa ada elemen baru dalam array.

Saya percaya bahwa ini semua konsekuensi dari Pada setiap bagian iterasi dari penjelasan dalam dokumentasi, yang mungkin berarti bahwa foreachmelakukan semua logika sebelum memanggil kode {}.

Kasus cobaan

Jika Anda menjalankan ini:

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

Anda akan mendapatkan hasil ini:

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

Yang berarti menerima modifikasi dan pergi melalui itu karena itu diubah "dalam waktu". Tetapi jika Anda melakukan ini:

<?
    $array = Array(
        'foo' => 1,
        'bar' => 2
    );
    foreach($array as $k=>&$v) {
        if ($k=='bar') {
            $array['baz']=3;
        }
        echo $v." ";
    }
    print_r($array);
?>

Kamu akan mendapatkan:

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

Yang berarti array telah dimodifikasi, tetapi karena kami memodifikasinya ketika elemen yang foreachsudah ada di array terakhir, itu "memutuskan" untuk tidak mengulang lagi, dan meskipun kami menambahkan elemen baru, kami menambahkannya "terlambat" dan itu tidak diulang.

Penjelasan terperinci dapat dibaca di Bagaimana cara PHP 'foreach' bekerja? yang menjelaskan internal di balik perilaku ini.

dkasipovic
sumber
7
Nah, apakah Anda membaca sisa jawabannya? Sangat masuk akal bahwa foreach memutuskan apakah akan mengulang di lain waktu bahkan sebelum menjalankan kode di dalamnya.
dkasipovic
2
Tidak, array diubah, tetapi "terlambat" karena foreach sudah "berpikir" bahwa itu adalah elemen terakhir (yang pada awal iterasi) dan tidak akan berulang lagi. Dimana dalam contoh kedua, itu bukan pada elemen terakhir di awal iterasi dan mengevaluasi lagi pada awal iterasi berikutnya. Saya mencoba menyiapkan test case.
dkasipovic
1
@AlmaDo Lihat lxr.php.net/xref/PHP_TRUNK/Zend/zend_vm_def.h#4509 Itu selalu diatur ke pointer berikutnya ketika iterates. Jadi, ketika mencapai iterasi terakhir, itu akan ditandai sebagai selesai (melalui pointer NULL). Ketika Anda kemudian menambahkan kunci dalam iterasi terakhir, foreach tidak akan menyadarinya.
bwoebi
1
@DKasipovic no. Tidak ada penjelasan lengkap & jelas di sana (setidaknya untuk saat ini - mungkin saya salah)
Alma Do
4
Sebenarnya sepertinya @AlmaDo memiliki kelemahan dalam memahami logikanya sendiri ... Jawaban Anda baik-baik saja.
bwoebi
15

Sesuai dokumentasi yang disediakan oleh manual PHP.

Pada setiap iterasi, nilai elemen saat ini ditetapkan ke $ v dan
pointer array internal dimajukan oleh satu (jadi pada iterasi berikutnya, Anda akan melihat elemen berikutnya).

Jadi sesuai contoh pertama Anda:

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

$arrayhanya memiliki satu elemen, sehingga sesuai dengan eksekusi sebelumnya, 1 ditugaskan $vdan tidak memiliki elemen lain untuk memindahkan pointer

Tetapi dalam contoh kedua Anda:

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

$arraymemiliki dua elemen, jadi sekarang $ array mengevaluasi indeks nol dan memindahkan pointer dengan satu. Untuk iterasi loop pertama, ditambahkan $array['baz']=3;sebagai referensi by pass.

pengguna3535130
sumber
13

Pertanyaan besar, karena banyak pengembang, bahkan yang berpengalaman, bingung dengan cara PHP menangani array di foreach loop. Dalam loop foreach standar, PHP membuat salinan array yang digunakan dalam loop. Salinan dibuang segera setelah loop selesai. Ini transparan dalam pengoperasian loop foreach sederhana. Sebagai contoh:

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

Output ini:

apple
banana
coconut

Jadi salinan dibuat tetapi pengembang tidak memperhatikan, karena array asli tidak direferensikan dalam loop atau setelah loop selesai. Namun, ketika Anda mencoba untuk memodifikasi item dalam satu lingkaran, Anda menemukan bahwa mereka tidak dimodifikasi ketika Anda selesai:

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

print_r($set);

Output ini:

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

Setiap perubahan dari yang asli tidak boleh menjadi pemberitahuan, sebenarnya tidak ada perubahan dari yang asli, meskipun Anda dengan jelas menetapkan nilai ke $ item. Ini karena Anda beroperasi pada $ item karena muncul dalam salinan $ set yang sedang dikerjakan. Anda dapat mengesampingkan ini dengan meraih $ item dengan referensi, seperti:

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

Output ini:

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

Jadi jelas dan dapat diamati, ketika $ item dioperasikan berdasarkan referensi, perubahan yang dilakukan ke $ item dilakukan kepada anggota dari set $ asli. Menggunakan $ item dengan referensi juga mencegah PHP membuat salinan array. Untuk menguji ini, pertama-tama kami akan menampilkan skrip cepat yang menunjukkan salinan:

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

Output ini:

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

Seperti yang ditunjukkan pada contoh, PHP menyalin $ set dan menggunakannya untuk mengulang, tetapi ketika $ set digunakan di dalam loop, PHP menambahkan variabel ke array asli, bukan array yang disalin. Pada dasarnya, PHP hanya menggunakan array yang disalin untuk mengeksekusi loop dan penugasan $ item. Karena itu, loop di atas hanya mengeksekusi 3 kali, dan setiap kali menambahkan nilai lain ke akhir dari set $ asli, meninggalkan $ set asli dengan 6 elemen, tetapi tidak pernah memasukkan loop infinite.

Namun, bagaimana jika kami menggunakan $ item dengan referensi, seperti yang saya sebutkan sebelumnya? Satu karakter ditambahkan ke tes di atas:

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

Hasil dalam loop tak terbatas. Perhatikan ini sebenarnya adalah infinite loop, Anda harus mematikan skrip sendiri atau menunggu OS Anda kehabisan memori. Saya menambahkan baris berikut ke skrip saya sehingga PHP akan kehabisan memori dengan sangat cepat, saya sarankan Anda melakukan hal yang sama jika Anda akan menjalankan tes loop tak terbatas ini:

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

Jadi dalam contoh sebelumnya dengan infinite loop, kita melihat alasan mengapa PHP ditulis untuk membuat salinan array untuk di-loop. Ketika salinan dibuat dan digunakan hanya oleh struktur loop itu sendiri, array tetap statis sepanjang eksekusi loop, sehingga Anda tidak akan pernah mengalami masalah.

Hrvoje Antunović
sumber
7

PHP foreach loop dapat digunakan dengan Indexed arrays, Associative arraysdan Object public variables.

Dalam foreach loop, hal pertama yang dilakukan php adalah membuat salinan array yang akan diulangi. PHP kemudian beralih ke copyarray baru ini daripada yang asli. Ini ditunjukkan dalam contoh di bawah ini:

<?php
$numbers = [1,2,3,4,5,6,7,8,9]; # initial values for our array
echo '<pre>', print_r($numbers, true), '</pre>', '<hr />';
foreach($numbers as $index => $number){
    $numbers[$index] = $number + 1; # this is making changes to the origial array
    echo 'Inside of the array = ', $index, ': ', $number, '<br />'; # showing data from the copied array
}
echo '<hr />', '<pre>', print_r($numbers, true), '</pre>'; # shows the original values (also includes the newly added values).

Selain itu, php juga memungkinkan untuk digunakan iterated values as a reference to the original array value. Ini ditunjukkan di bawah ini:

<?php
$numbers = [1,2,3,4,5,6,7,8,9];
echo '<pre>', print_r($numbers, true), '</pre>';
foreach($numbers as $index => &$number){
    ++$number; # we are incrementing the original value
    echo 'Inside of the array = ', $index, ': ', $number, '<br />'; # this is showing the original value
}
echo '<hr />';
echo '<pre>', print_r($numbers, true), '</pre>'; # we are again showing the original value

Catatan: Ini tidak memungkinkan original array indexesuntuk digunakan sebagai references.

Sumber: http://dwellupper.io/post/47/understanding-php-foreach-loop-with-examples

Pranav Rana
sumber
1
Object public variablessalah atau paling tidak menyesatkan. Anda tidak dapat menggunakan objek dalam array tanpa antarmuka yang benar (misalnya, Traversible) dan ketika Anda melakukannya, foreach((array)$obj ...Anda sebenarnya bekerja dengan array sederhana, bukan objek lagi.
Christian