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 Traversable
objek, karena ini foreach
pada 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 Iterator
antarmuka 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 $arr
diubah 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 foreach
memanfaatkan IAP, ada komplikasi tambahan: Hanya ada satu IAP, tetapi satu array dapat menjadi bagian dari banyak foreach
loop:
// 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, foreach
lakukan shenanigans berikut: Sebelum loop body dieksekusi, foreach
akan 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 current
keluarga fungsi), karena itu perubahan pada jumlah IAP sebagai modifikasi di bawah semantik copy-on-write. Sayangnya, ini berarti bahwa foreach
dalam 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
refcount
1, maka array tidak dibagi dan kita bebas untuk memodifikasinya secara langsung.
Jika array tidak diduplikasi (is_ref = 0, refcount = 1), maka hanya arraynya yang refcount
akan bertambah (*). Selain itu, jika foreach
dengan 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, $arr
akan digandakan untuk mencegah perubahan IAP $arr
dari 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 refcount
sini 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 refcount
dan 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 unset
menjadi elemen berikutnya , bukan yang sekarang.
Contoh: Kasing uji Anda
Tiga aspek yang dijelaskan di atas akan memberi Anda kesan yang lengkap tentang keanehan foreach
implementasi dan kami dapat melanjutkan untuk membahas beberapa contoh.
Perilaku kasus pengujian Anda mudah dijelaskan pada titik ini:
Dalam kasus uji 1 dan 2 $array
dimulai dengan refcount = 1, jadi itu tidak akan diduplikasi oleh foreach
: Hanya yang refcount
bertambah. 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 foreach
akan memodifikasi IAP dari $array
variabel. Pada akhir iterasi, IAP adalah NULL (artinya iterasi telah dilakukan), yang each
menunjukkan dengan mengembalikan false
.
Dalam kasus uji 4 dan 5 keduanya each
dan reset
merupakan fungsi referensi. The $array
memiliki refcount=2
ketika diberikan kepada mereka, sehingga harus digandakan. Dengan demikian foreach
akan bekerja pada array yang terpisah lagi.
Contoh: Efek current
ineach
Cara yang baik untuk menunjukkan berbagai perilaku duplikasi adalah dengan mengamati perilaku current()
fungsi di dalam foreach
loop. 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 next
yang semuanya oleh-ref. Pass-reference passing menyiratkan bahwa array harus dipisahkan dan dengan demikian $array
dan foreach-array
akan berbeda. Alasan Anda mendapatkan 2
alih-alih 1
juga disebutkan di atas: foreach
memajukan pointer array sebelum menjalankan kode pengguna, bukan setelahnya. Jadi meskipun kode berada di elemen pertama, foreach
sudah 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 foreach
bekerja pada array yang sama. Anda masih melihat perilaku off-by-one, karena cara foreach
memajukan 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 $array
is_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 $array
adalah 2 ketika loop dimulai, jadi untuk sekali ini kita benar-benar harus melakukan duplikasi dimuka. Dengan demikian $array
dan 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 1
telah 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 HashPointer
mekanisme 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 HashPointer
pengembalian 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 foreach
meyakini 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, 4
sesuai 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 foreach
tidak akan lagi menggunakan IAP sama sekali . The foreach
Loop 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 refcount
peningkatan (bukan duplikasi array) dalam semua kasus. Jika array diubah selama foreach
loop, pada saat itu duplikasi akan terjadi (sesuai dengan copy-on-write) dan foreach
akan 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 refcounting
dan perilaku duplikasi persis sama antara PHP 5 dan PHP 7).
Perubahan test case 3: Foreach
tidak 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 foreach
masih menggunakan array asli. (Bukan berarti perubahan IAP akan menjadi masalah, bahkan jika array dibagikan.)
Rangkaian contoh kedua terkait dengan perilaku di current()
bawah reference/refcounting
konfigurasi 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.