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:
- Array bukan referensi (is_ref = 0). Jika ini adalah referensi, maka perubahan yang seharusnya diperbanyak, sehingga tidak boleh digandakan.
- 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.