Pertanyaan Bagaimana PHP 'foreach' benar-benar berfungsi?


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


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

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

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

Test case 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 kita tidak bekerja secara langsung dengan array sumber - jika tidak, loop akan terus berlanjut selamanya, karena kita terus mendorong item ke array selama loop. Tetapi hanya untuk memastikan inilah masalahnya:

Test case 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 dari array sumber selama loop, kalau tidak kita akan melihat nilai-nilai yang dimodifikasi selama loop. Tapi...

Jika kita melihat di manual, kami menemukan pernyataan ini:

Ketika foreach pertama mulai mengeksekusi, pointer array internal secara otomatis diatur ulang ke elemen pertama dari array.

Benar ... ini sepertinya menyarankan itu foreach bergantung pada pointer array dari array sumber. Tapi kami baru saja membuktikan bahwa kami tidak berfungsi dengan array sumberbenar? Yah, tidak sepenuhnya.

Test case 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 fakta bahwa kita tidak bekerja secara langsung dengan array sumber, kita bekerja secara langsung dengan pointer array sumber - fakta bahwa pointer berada di ujung array pada akhir loop menunjukkan ini. Kecuali ini tidak mungkin benar - kalau begitu, kalau begitu test case 1 akan berputar selamanya.

Panduan PHP juga menyatakan:

Karena foreach bergantung pada penunjuk array internal yang mengubahnya dalam loop dapat menyebabkan perilaku yang tidak diharapkan.

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

Test case 4:

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

/* Output: 1 2 3 4 5 */

Test case 5:

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

/* Output: 1 2 3 4 5 */

... tidak ada yang tidak terduga di sana, nyatanya tampaknya mendukung teori "copy of source".


Pertanyaan

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

Tampaknya bagi saya bahwa foreach bekerja dengan salinan dari array, tetapi mengatur pointer array dari array sumber ke ujung array setelah loop.

  • Apakah ini benar dan keseluruhan cerita?
  • Jika tidak, apa yang sebenarnya dilakukannya?
  • Apakah ada situasi di mana menggunakan fungsi yang menyesuaikan pointer array (each(), reset() et al.) selama a foreach dapat mempengaruhi hasil dari loop?

1637
2018-04-07 19:33


asal


Jawaban:


foreach mendukung iterasi atas tiga jenis nilai yang berbeda:

Berikut ini saya akan mencoba menjelaskan dengan tepat bagaimana iterasi bekerja dalam berbagai kasus. Sejauh ini kasus yang paling sederhana adalah Traversable benda-benda, seperti untuk ini foreach pada dasarnya hanya gula sintaks untuk kode sepanjang garis-garis 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, pemanggilan metode yang sebenarnya dihindari dengan menggunakan API internal yang pada dasarnya hanya mencerminkan Iterator antarmuka pada level C.

Iterasi array dan objek biasa secara signifikan lebih rumit. Pertama-tama, perlu dicatat bahwa dalam PHP "array" adalah kamus yang benar-benar dipesan dan mereka akan dilacak menurut urutan ini (yang cocok dengan urutan penyisipan selama Anda tidak menggunakan sesuatu seperti sort). Ini bertentangan dengan iterasi oleh tatanan alami kunci (bagaimana daftar dalam bahasa lain sering berhasil) atau tidak memiliki urutan yang ditentukan sama sekali (bagaimana kamus dalam bahasa lain sering berfungsi).

Hal yang sama juga berlaku untuk objek, karena properti objek dapat dilihat sebagai yang lain (dipesan) kamus pemetaan nama properti ke nilainya, ditambah beberapa penanganan visibilitas. Dalam sebagian besar kasus, properti objek sebenarnya tidak disimpan dalam cara yang agak tidak efisien ini. Namun jika Anda memulai iterasi atas objek, representasi yang dikemas yang biasanya digunakan akan dikonversi ke kamus nyata. Pada titik itu, iterasi objek biasa menjadi sangat mirip dengan iterasi array (yang mengapa saya tidak membahas iterasi plain-object banyak di sini).

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

  • Jika Anda mengulang dengan referensi menggunakan foreach ($arr as &$v) kemudian $arr diubah menjadi referensi dan Anda dapat mengubahnya selama iterasi.
  • Di PHP 5 hal yang sama berlaku bahkan jika Anda iterate berdasarkan nilai, tetapi array adalah referensi sebelumnya: $ref =& $arr; foreach ($ref as $v)
  • Objek memiliki by-handle lewat semantik, yang untuk tujuan praktis harus berarti bahwa mereka berperilaku seperti referensi. Jadi objek dapat selalu diubah selama iterasi.

Masalah dengan modifikasi yang memungkinkan 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 akan ditinggalkan dengan penunjuk menjuntai (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 edge-case yang aneh, sementara pendekatan yang lebih melibatkan PHP 7 menghasilkan perilaku yang lebih dapat diprediksi dan konsisten.

Sebagai pendahuluan terakhir, perlu 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 menaikkan jumlah referensi (refcount). Hanya sekali Anda melakukan semacam modifikasi, salinan asli (disebut "duplikasi") akan dilakukan. Lihat Anda dibohongi untuk pengenalan yang lebih luas tentang topik ini.

PHP 5

Array array internal dan HashPointer

Array di PHP 5 memiliki satu "penunjuk array internal" (IAP) yang khusus, yang mendukung modifikasi dengan benar: Setiap kali elemen dihapus, akan ada pemeriksaan apakah IAP mengarah ke elemen ini. Jika ya, itu maju ke elemen selanjutnya.

Sementara foreach memang memanfaatkan IAP, ada komplikasi tambahan: Hanya ada satu IAP, tetapi satu larik dapat menjadi bagian dari beberapa loop foreach:

// 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 penunjuk array internal, foreach melakukan schenanigans berikut: Sebelum badan loop dijalankan, foreach akan mem-back-up pointer ke elemen saat ini dan hashnya ke per-foreach HashPointer. Setelah putaran badan berjalan, IAP akan diatur kembali ke elemen ini jika masih ada. Namun jika elemen telah dihapus, kami hanya akan menggunakan dimanapun IAP saat ini. Skema ini sebagian besar agak-agak bekerja, tetapi ada banyak perilaku aneh yang bisa Anda dapatkan darinya, beberapa di antaranya akan saya tunjukkan di bawah.

Duplikasi rangkai

IAP adalah fitur yang terlihat dari sebuah array (terkena melalui current keluarga fungsi), seperti perubahan pada perhitungan IAP sebagai modifikasi di semantik copy-on-write. Sayangnya ini berarti bahwa foreach dalam banyak kasus dipaksa untuk menduplikasi array iterasi berulang. Kondisi yang tepat adalah:

  1. Array bukan referensi (is_ref = 0). Jika itu referensi, maka ubahlah itu seharusnya untuk menyebar, jadi tidak boleh diduplikasi.
  2. Array memiliki refcount> 1. Jika refcount adalah 1, maka array tidak dibagi dan kami bebas untuk mengubahnya secara langsung.

Jika array tidak diduplikasi (is_ref = 0, refcount = 1), maka hanya refcount yang akan bertambah (*). Selain itu, jika foreach oleh referensi digunakan, maka array (berpotensi digandakan) 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($arr);

Sini, $arr akan diduplikasi untuk mencegah IAP berubah $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 perhatian terhadap modifikasi selama iterasi di sini, jadi kami tidak benar-benar perlu menggunakan IAP di tempat pertama).

(*) Incrementing the refcount disini terdengar tidak berbahaya, tetapi melanggar semantik copy-on-write (COW): Ini berarti bahwa kita akan memodifikasi IAP dari refcount = 2 array, sementara COW menyatakan bahwa modifikasi hanya dapat dilakukan pada refcount = 1 nilai. Pelanggaran ini menghasilkan perubahan perilaku yang terlihat pengguna (sementara COW biasanya transparan), karena perubahan IAP pada array yang diulang akan dapat diamati - tetapi hanya sampai modifikasi non-IAP pertama pada larik. Sebagai gantinya, tiga opsi "valid" akan menjadi a) untuk selalu menduplikasi, b) untuk tidak menaikkan jumlah refcount dan dengan demikian memungkinkan array iterasi diubah secara sewenang-wenang dalam loop, atau c) tidak menggunakan IAP sama sekali ( solusi PHP 7).

Urutan posisi promosi

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

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

Namun foreach, menjadi 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();
}

Yakni, pointer array sudah bergerak maju sebelum tubuh loop berjalan. Ini berarti bahwa sementara tubuh loop bekerja pada elemen $i, IAP sudah di elemen $i+1. Ini adalah alasan mengapa contoh kode yang menunjukkan modifikasi selama iterasi akan selalu membatalkan pengaturan berikutnya elemen, daripada yang sekarang.

Contoh: Kasus uji Anda

Tiga aspek yang diuraikan di atas seharusnya memberi Anda kesan yang sangat lengkap tentang keistimewaan dari penerapan foreach dan kita dapat melanjutkan dengan membahas beberapa contoh.

Perilaku kasus uji Anda mudah dijelaskan pada titik ini:

  • Dalam kasus uji 1 dan 2 $array dimulai dengan refcount = 1, sehingga tidak akan diduplikasi oleh foreach: Hanya refcount yang bertambah. Ketika tubuh loop kemudian memodifikasi array (yang memiliki refcount = 2 pada titik itu), duplikasi akan terjadi pada titik tersebut. Foreach akan terus mengerjakan salinan yang tidak dimodifikasi $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 selesai), yang each menunjukkan dengan kembali false.

  • Dalam kasus uji 4 dan 5 keduanya each dan reset adalah fungsi referensi. Itu $array mempunyai sebuah refcount=2 ketika itu diteruskan kepada mereka, jadi itu harus diduplikasi. Dengan demikian foreach akan bekerja pada susunan terpisah lagi.

Contoh: Efek current di foreach

Cara yang baik untuk menunjukkan berbagai perilaku duplikasi adalah mengamati perilaku current() berfungsi di dalam loop foreach. Pertimbangkan contoh ini:

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

Di sini Anda harus tahu itu current() adalah fungsi by-ref (sebenarnya: prefer-ref), meskipun tidak mengubah array. Itu harus dalam rangka untuk bermain bagus dengan semua fungsi lain seperti next yang semuanya oleh-ref. By-reference passing menyiratkan bahwa array harus dipisahkan dan dengan demikian $array dan foreach-array akan berbeda. Alasan Anda dapatkan 2 dari pada 1 juga disebutkan di atas: foreach memajukan pointer array sebelum menjalankan kode pengguna, bukan setelahnya. Jadi meskipun kode berada di elemen pertama, foreach sudah meng-advanced 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 is_ref = 1 case, jadi array tidak disalin (seperti di atas). Tapi sekarang itu adalah referensi, array tidak lagi harus diduplikasi ketika diteruskan ke refre current() fungsi. Demikian current() dan foreach bekerja pada array yang sama. Anda masih melihat perilaku off-by-one, karena caranya foreachmemajukan penunjuk.

Anda mendapatkan perilaku yang sama saat melakukan iterasi-ulang:

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 sebuah 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 larik ke variabel lain:

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

Di sini jumlah dari $array adalah 2 ketika loop dimulai, jadi untuk sekali kita benar-benar harus melakukan duplikasi dimuka. Demikian $array dan array yang digunakan oleh foreach akan benar-benar terpisah dari awal. Itulah mengapa Anda mendapatkan posisi IAP di mana pun sebelum loop (dalam hal ini berada di posisi pertama).

Contoh: Modifikasi selama iterasi

Mencoba untuk memperhitungkan modifikasi selama iterasi adalah di mana semua masalah foreach kita 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 bahwa 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 itu (1, 2) hilang dari output, karena elemen 1 telah dihapus. Apa yang mungkin tidak terduga adalah loop luar berhenti setelah elemen pertama. Mengapa demikian?

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

Konsekuensi lain dari HashPointer mekanisme backup + restore adalah perubahan pada IAP reset() dll. biasanya tidak berdampak foreach. Sebagai contoh, kode berikut mengeksekusi 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, sementara itu reset() untuk sementara memodifikasi IAP, itu akan dikembalikan ke elemen foreach saat ini setelah badan loop. Untuk memaksa reset() untuk membuat efek pada loop, Anda harus juga menghapus elemen saat ini, sehingga mekanisme cadangan / pemulihan 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. Kesenangan nyata dimulai jika Anda ingat bahwa HashPointer restore menggunakan pointer ke elemen dan hashnya untuk menentukan apakah masih ada. Namun: Hash memiliki benturan, dan pointer dapat digunakan kembali! Ini berarti bahwa, dengan pilihan tombol susunan yang hati-hati, kita bisa membuatnya foreach percaya 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 itu '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 lingkaran.

Mengganti entitas yang diulang selama pengulangan

Satu kasus terakhir yang ingin saya sebutkan, adalah bahwa PHP memungkinkan Anda untuk mengganti entitas yang diulang selama pengulangan. Jadi Anda dapat memulai iterasi pada satu larik dan kemudian menggantinya dengan larik lainnya di tengah. Atau mulai iterasi pada larik lalu gantilah 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 kasus ini, PHP hanya akan memulai iterasi entitas lain dari awal setelah penggantian telah terjadi.

PHP 7

Iterator Hashtable

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

PHP 7 menggunakan pendekatan yang berbeda, yaitu mendukung pembuatan jumlah acak dari iterasi hashtable eksternal yang aman. Iterator ini harus terdaftar dalam larik, dari titik mana mereka memiliki semantik yang sama dengan IAP: Jika elemen larik dihapus, semua iterator hashtable yang menunjuk ke elemen tersebut akan dimajukan ke elemen berikutnya.

Ini berarti bahwa foreach tidak akan lagi menggunakan IAP sama sekali. Perulangan foreach akan sama sekali tidak berpengaruh pada hasil current() dll. dan perilaku sendiri tidak akan pernah dipengaruhi oleh fungsi seperti reset() dll.

Duplikasi rangkai

Perubahan penting lainnya antara PHP 5 dan PHP 7 berkaitan dengan duplikasi susunan. Sekarang bahwa IAP tidak lagi digunakan, iterasi array nilai hanya akan melakukan peningkatan refcount (bukan duplikasi array) dalam semua kasus. Jika array dimodifikasi selama loop foreach, pada saat itu duplikasi akan terjadi (menurut copy-on-write) dan foreach akan tetap bekerja pada array lama.

Dalam banyak kasus, perubahan ini transparan dan tidak memiliki efek lain 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 iterasi referensi-array adalah kasus khusus. Dalam hal ini tidak terjadi duplikasi, jadi semua modifikasi dari array selama iterasi akan dipantulkan oleh loop. Dalam PHP 7 kasus khusus ini hilang: A-nilai iterasi dari suatu array akan selalu tetap bekerja pada elemen asli, tanpa menghiraukan modifikasi apa pun selama pengulangan.

Ini, tentu saja, tidak berlaku untuk referensi iterasi. Jika Anda iterasikan oleh-referensi semua modifikasi akan dipantulkan oleh loop. Menariknya, hal yang sama berlaku untuk iterasi dengan nilai dari objek biasa:

$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 dipegang (yaitu, mereka bertingkah laku mirip referensi bahkan dalam konteks nilai).

Contoh

Mari kita pertimbangkan beberapa contoh, dimulai dengan kasus uji Anda:

  • Uji kasus 1 dan 2 mempertahankan output yang sama: By-value array iteration selalu terus bekerja pada elemen asli. (Dalam hal ini bahkan perilaku penghitungan ulang dan duplikasi sama persis 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.

  • Uji kasus 4 dan 5 tetap sama: each() dan reset() akan menduplikasi larik sebelum mengubah IAP, sementara foreach masih menggunakan larik asli. (Bukan berarti perubahan IAP akan menjadi masalah, bahkan jika array itu dibagikan.)

Kumpulan contoh kedua terkait dengan perilaku current() di bawah referensi / refcounting konfigurasi yang berbeda. Ini tidak lagi masuk akal, seperti current() sama sekali tidak terpengaruh oleh loop, jadi nilai kembalinya selalu tetap sama.

Namun, kami mendapatkan beberapa perubahan yang 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 berhenti setelah iterasi pertama. Alasannya adalah bahwa kedua loop sekarang memiliki iterator hashtable sepenuhnya terpisah, dan tidak ada lagi kontaminasi silang dari kedua loop melalui IAP bersama.

Kasus tepi aneh lainnya 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 hapus (karena hash bertabrakan dan penunjuk). Karena kami tidak lagi bergantung pada elemen hash untuk apa pun, ini bukan lagi masalah.


1378
2018-02-13 13:21



Dalam contoh 3 Anda tidak memodifikasi larik. Dalam semua contoh lain Anda memodifikasi konten atau penunjuk array internal. Ini penting dalam hal ini PHP array karena semantik operator penugasan.

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

Berikut ini contohnya:

$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 uji Anda, Anda dapat dengan mudah membayangkannya foreach membuat semacam iterator dengan referensi ke array. Referensi ini berfungsi persis seperti variabel $b dalam contoh saya. Namun, iterator beserta referensi hanya hidup selama pengulangan dan kemudian, keduanya dibuang. Sekarang Anda dapat melihat bahwa, dalam semua kasus tetapi 3, array diubah selama loop, sementara referensi tambahan ini hidup. Ini memicu klon, dan itu menjelaskan apa yang terjadi di sini!

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


97
2018-04-07 20:43



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

Sebuah) foreach bekerja pada salinan prospektif dari array asli.     Itu berarti foreach () akan memiliki penyimpanan data SHARED sampai atau kecuali a prospected copy aku s     tidak dibuat foreach Catatan / Komentar pengguna.

b) Apa yang memicu a salinan prospektif?     Salinan prospektif dibuat berdasarkan kebijakan copy-on-write, yaitu, kapan saja     array yang dilewatkan ke 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 , Iterator, dan Array Iterator.

Pertanyaan Stack Overflow Bagaimana cara memastikan nilai disetel ulang dalam loop 'foreach' di PHP? alamat kasus (3,4,5) pertanyaan Anda.

Contoh berikut menunjukkan bahwa masing-masing () dan reset () TIDAK mempengaruhi SENTINEL variabel (for example, the current index variable) dari iterator foreach ()

$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

34
2018-04-07 21:03



CATATAN UNTUK PHP 7

Untuk memperbarui jawaban ini karena telah mendapatkan beberapa popularitas: Jawaban ini tidak lagi berlaku pada PHP 7. Seperti yang dijelaskan dalam "Perubahan mundur yang tidak kompatibel", dalam PHP 7 foreach bekerja pada salinan dari array, jadi setiap perubahan pada array itu sendiri tidak tercermin pada foreach loop. Lebih jelasnya 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   penunjuk array internal dimajukan oleh satu (seterusnya berikutnya   iterasi, Anda akan melihat elemen selanjutnya).

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 tidak pada elemen terakhir sehingga mengevaluasi array pada iterasi berikutnya dan dengan demikian menyadari bahwa ada elemen baru dalam larik.

Saya percaya bahwa ini semua adalah konsekuensi dari Pada setiap iterasi bagian dari penjelasan dalam dokumentasi, yang mungkin berarti itu foreach melakukan 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 output ini:

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

Yang berarti bahwa ia menerima modifikasi dan melewatinya karena dimodifikasi "pada waktunya". 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 bahwa array telah dimodifikasi, tetapi karena kami mengubahnya ketika foreach sudah berada di elemen terakhir dari array, itu "memutuskan" untuk tidak loop lagi, dan meskipun kami menambahkan elemen baru, kami menambahkannya "terlambat" dan itu tidak dilewati.

Penjelasan detail bisa dibaca di Bagaimana PHP 'foreach' benar-benar berfungsi? yang menjelaskan internal di balik perilaku ini.


22
2018-04-15 08:46



Sesuai dokumentasi yang disediakan oleh manual PHP.

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

Jadi sesuai contoh pertama Anda:

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

$array hanya memiliki elemen tunggal, sehingga sesuai dengan eksekusi foreach, 1 assign to $vdan tidak memiliki elemen lain untuk memindahkan penunjuk

Tetapi dalam contoh kedua Anda:

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

$array memiliki dua elemen, jadi sekarang $ array mengevaluasi indeks nol dan menggerakkan penunjuk oleh satu. Untuk iterasi loop pertama, tambah $array['baz']=3; sebagai lewat referensi.


8
2018-04-15 09:32



Pertanyaan bagus, karena banyak pengembang, bahkan yang berpengalaman, yang bingung dengan cara PHP menangani array dalam foreach loops. Dalam loop foreach standar, PHP membuat salinan dari 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 mengubah item dalam satu lingkaran, Anda menemukan bahwa item tersebut tidak dimodifikasi saat 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
)

Perubahan apa pun dari yang asli tidak dapat menjadi pemberitahuan, sebenarnya tidak ada perubahan dari yang asli, meskipun Anda dengan jelas memberikan nilai ke $ item. Ini karena Anda beroperasi pada $ item seperti yang tampak pada salinan $ set yang sedang dikerjakan. Anda dapat menimpanya dengan mengambil $ 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 dibuat ke $ item dilakukan ke anggota $ set asli. Menggunakan $ item dengan referensi juga mencegah PHP membuat salinan array. Untuk menguji ini, pertama-tama kami akan menunjukkan 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 loop over, 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 eksekusi loop dan penugasan $ item. Karena ini, loop di atas hanya mengeksekusi 3 kali, dan setiap kali menambahkan nilai lain ke akhir $ set asli, meninggalkan $ set asli dengan 6 elemen, tetapi tidak pernah memasukkan loop tak terbatas.

Namun, bagaimana jika kami telah 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);

Hasilnya dalam lingkaran tak terbatas. Perhatikan ini sebenarnya adalah pengulangan tak terbatas, Anda harus membunuh skrip itu 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 pengujian loop tak terbatas ini:

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

Jadi dalam contoh sebelumnya ini dengan loop tak terbatas, kita melihat alasan mengapa PHP ditulis untuk membuat salinan dari array untuk diulang. Ketika salinan dibuat dan hanya digunakan oleh struktur loop yang membangun dirinya, array tetap statis selama eksekusi loop, sehingga Anda tidak akan mengalami masalah.


5
2018-04-21 08:44



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

Dalam foreach loop, hal pertama yang dilakukan php adalah membuat salinan dari array yang akan diulang. PHP kemudian mengulangi ini baru copy dari array 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 memang memungkinkan untuk digunakan iterated values as a reference to the original array value demikian juga. 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: Itu tidak memungkinkan original array indexes untuk digunakan sebagai references.

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


5
2017-11-13 14:08