Bagaimana sebenarnya cara kerja PHP ‘foreach’?

Bagaimana sebenarnya cara kerja PHP ‘foreach’?

iis

August 16, 2019

Biarkan saya awali ini dengan mengatakan bahwa saya tahu apa yang dimaksud dengan foreach, apakah dan bagaimana menggunakannya. Pertanyaan ini menyangkut 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 berasumsi bahwa foreach bekerja 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 eksperimen menemukan bahwa ini sebenarnya tidak 100% benar.

php

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-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 foreach bergantung 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”.

foreach mendukung iterasi atas tiga jenis nilai:

  • Array
  • Normal objects
  • Traversable objects

Berikut ini, saya akan mencoba menjelaskan dengan tepat bagaimana iterasi bekerja dalam berbagai kasus. Sejauh ini kasus yang paling sederhana adalah objek Traversable, karena foreach ini pada dasarnya hanya sintaksis gula 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 antarmuka Iterator di tingkat 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 dilalui sesuai dengan urutan ini (yang cocok dengan urutan penyisipan selama Anda tidak menggunakan sesuatu seperti semacam). 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 (memerintahkan) nama properti pemetaan 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 baik. 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 mengulanginya dengan referensi menggunakan foreach ($ arr as & $ v) maka $ arr diubah menjadi referensi dan Anda dapat mengubahnya saat 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-handle semantik yang lewat, 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 array Anda saat ini. Jika elemen ini sekarang dibebaskan, Anda dibiarkan dengan pointer menggantung (biasanya menghasilkan segfault).

Ada berbagai cara untuk memecahkan masalah ini. PHP 5 dan PHP 7 berbeda secara signifikan dalam hal ini dan saya akan menjelaskan kedua perilaku berikut ini. Ringkasannya adalah bahwa pendekatan PHP 5 agak bodoh dan mengarah ke semua jenis masalah tepi yang 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 lama dan menambah jumlah referensi (refcount). Hanya sekali Anda melakukan beberapa jenis modifikasi, salinan asli (disebut “duplikasi”) akan dilakukan.