PHP 'foreach' aslında nasıl çalışır?


2018

Ne foreacholduğunu, yaptığını ve nasıl kullanılacağını bildiğimi söyleyerek ön ekleyeyim . Bu soru kaporta altında nasıl çalıştığı ile ilgilidir ve ben "Bu bir dizi ile döngü nasıl" satırlarında herhangi bir cevap istemiyorum foreach.


Uzun bir süre foreachdizinin kendisi ile çalıştığını varsaydım . Sonra dizinin bir kopyasıyla çalıştığı gerçeğine birçok referans buldum ve o zamandan beri bunun hikayenin sonu olduğunu varsaydım. Ancak son zamanlarda konuyla ilgili bir tartışmaya girdim ve küçük bir deneyden sonra bunun aslında% 100 doğru olmadığını gördüm.

Ne demek istediğimi göstereyim. Aşağıdaki test durumları için, aşağıdaki diziyle çalışacağız:

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

Test örneği 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 */

Bu, doğrudan kaynak dizisiyle çalışmadığımızı açıkça gösteriyor - aksi takdirde döngü, öğeleri döngü boyunca sürekli olarak dizinin üzerine ittiğimiz için döngü sonsuza kadar devam edecektir. Ama sadece durumun böyle olduğundan emin olmak için:

Test örneği 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 */

Bu, ilk sonucumuzu yedekliyor, döngü sırasında kaynak dizinin bir kopyasıyla çalışıyoruz, aksi takdirde döngü sırasında değiştirilen değerleri görürüz. Fakat...

El kitabına bakarsak , şu ifadeyi buluruz:

Her foreach yürütmeye ilk başladığında, dahili dizi işaretçisi otomatik olarak dizinin ilk öğesine sıfırlanır.

Doğru ... Bu foreach, kaynak dizinin dizi işaretçisine dayandığını düşündürüyor . Ama kaynak diziyle çalışmadığımızı kanıtladık , değil mi? Tamamen değil.

Test örneği 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)
*/

Bu nedenle, doğrudan kaynak dizisi ile çalışmadığımız gerçeğine rağmen, doğrudan kaynak dizisi işaretçisi ile çalışıyoruz - işaretçinin döngünün sonunda dizinin sonunda olması gerçeği bunu gösterir. Bu doğru olamaz - eğer öyleyse, test durumu 1 sonsuza kadar dönecekti.

PHP el kitabı şunları da belirtir:

Foreach dahili dizi işaretçisine bağlı olduğu için döngü içinde onu değiştirmek beklenmedik davranışlara yol açabilir.

Peki, "beklenmedik davranış" ın ne olduğunu öğrenelim (teknik olarak, artık ne bekleyeceğimi bilmediğim için herhangi bir davranış beklenmedik).

Test durumu 4 :

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

/* Output: 1 2 3 4 5 */

Test durumu 5 :

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

/* Output: 1 2 3 4 5 */

... orada beklenmedik bir şey yok, aslında "kaynağın kopyası" teorisini destekliyor gibi görünüyor.


Soru

Burada neler oluyor? Benim C-fu sadece PHP kaynak koduna bakarak uygun bir sonuç elde edebilmek için yeterince iyi değil, birisi benim için İngilizce'ye çevirebilir eğer takdir ediyorum.

Bana öyle geliyor ki , dizinin foreachbir kopyasıyla çalışır , ancak kaynak dizinin dizi işaretçisini döngüden sonra dizinin sonuna ayarlar.

  • Bu doğru ve tüm hikaye doğru mu?
  • Değilse, gerçekten ne yapıyor?
  • A sırasında dizi işaretçisini ( each(), reset()et al.) Ayarlayan işlevlerin kullanılmasının foreach, döngünün sonucunu etkileyebileceği herhangi bir durum var mı ?

5
@DaveRandom Muhtemelen devam etmesi gereken bir php-internals etiketi var, ancak diğer 5 etiketten hangisinin değiştirileceğine karar vermek için size bırakacağım.
Michael Berkowski

5
silme tutamacı olmadan COW gibi görünüyor
zb '

149
İlk başta gosh, başka bir acemi soru. Dokümanları okuyun… hm, açıkça tanımlanmamış davranış «. Sonra tüm soruyu okudum ve şunu söylemeliyim: Sevdim. Bu konuda oldukça çaba sarf ettiniz ve tüm test senaryolarını yazdınız. ps. testcase 4 ve 5 aynı mıdır?
knittl

21
Neden dizi işaretçisinin dokunulduğunu anlamasının bir düşüncesi var: PHP orijinal dizinin dahili dizi işaretçisini kopyayla birlikte sıfırlamalı ve taşımalıdır, çünkü kullanıcı geçerli değere ( foreach ($array as &$value)) başvurmak isteyebilir PHP, bir kopya üzerinden yinelenmesine rağmen orijinal dizideki geçerli konumu bilmelidir.
Niko

4
@Sean: IMHO, PHP belgelerinin çekirdek dil özelliklerinin nüanslarını tanımlamakta oldukça kötü. Ama bu, belki de, çünkü çok sayıda özel özel dil dile pişirilir ...
Oliver Charlesworth

Yanıtlar:


1660

foreach üç farklı değer türünde yinelemeyi destekler:

Aşağıda, farklı durumlarda yinelemenin nasıl çalıştığını tam olarak açıklamaya çalışacağım. Şimdiye kadar en basit durum Traversablenesnelerdir, çünkü bunlar foreachiçin bu satırlar boyunca kod için sadece sözdizimi şekeri vardır:

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

Dahili sınıflar için, gerçek yöntem çağrıları, esasen sadece IteratorC seviyesindeki arayüzü yansıtan bir dahili API kullanılarak önlenir .

Dizilerin ve düz nesnelerin yinelenmesi önemli ölçüde daha karmaşıktır. Her şeyden önce, PHP "dizileri" gerçekten sipariş sözlükler olduğunu ve bu sırayla (gibi ekleme bir şey kullanmadığınız sürece ekleme sırası ile eşleşir) geçecektir belirtilmelidir sort. Bu, anahtarların doğal düzeniyle (diğer dillerdeki listelerin genellikle nasıl çalıştığı) veya hiç tanımlanmış bir düzene (diğer dillerdeki sözlüklerin genellikle nasıl çalıştığı) yinelemeye karşıdır.

Nesne özellikleri, değerlerine başka (sıralı) sözlük eşleme özellik adları ve bazı görünürlük işleme olarak da görülebildiği için aynı şey nesneler için de geçerlidir. Çoğu durumda, nesne özellikleri aslında bu oldukça verimsiz bir şekilde saklanmaz. Ancak, bir nesne üzerinde yinelemeye başlarsanız, normalde kullanılan paketlenmiş gösterim gerçek bir sözlüğe dönüştürülür. Bu noktada, düz nesnelerin yinelenmesi, dizilerin yinelenmesine çok benzer hale gelir (bu yüzden burada düz nesne yinelemesini çok fazla tartışmıyorum).

Çok uzak çok iyi. Bir sözlüğün üzerinde tekrar etmek çok zor olamaz, değil mi? Bir dizi / nesnenin yineleme sırasında değişebileceğini fark ettiğinizde sorunlar başlar. Bunun birden fazla yolu olabilir:

  • Kullandığınız referans olarak yineleme durumunda foreach ($arr as &$v)o zaman $arrbir referans haline getirilmiştir ve yineleme sırasında değiştirebilirsiniz.
  • PHP 5 için de, değere göre yineleme yapsanız bile aynı şey geçerlidir, ancak dizi önceden bir referanstı: $ref =& $arr; foreach ($ref as $v)
  • Nesneler, elleçleme geçiş semantiğine sahiptir, bu da en pratik amaçlar için referanslar gibi davrandıkları anlamına gelir. Böylece nesneler yineleme sırasında her zaman değiştirilebilir.

Yineleme sırasında değişiklik yapılmasına izin verme sorunu, üzerinde bulunduğunuz öğenin kaldırılması durumudur. Şu anda hangi dizi öğesini bulunduğunuzu takip etmek için bir işaretçi kullandığınızı varsayalım. Bu öğe şimdi serbest bırakılırsa, sarkan bir işaretçi bırakılır (genellikle bir segfault ile sonuçlanır).

Bu sorunu çözmenin farklı yolları vardır. PHP 5 ve PHP 7 bu konuda önemli ölçüde farklılık gösterir ve aşağıdaki iki davranışı da açıklayacağım. Özet, PHP 5'in yaklaşımının oldukça aptalca olması ve her türlü garip son durum sorununa yol açması, PHP 7'nin daha ilgili yaklaşımının ise daha öngörülebilir ve tutarlı davranışlarla sonuçlanmasıdır.

Son bir ön hazırlık olarak PHP'nin hafızayı yönetmek için referans sayma ve yazma üzerine kopyalama kullandığı belirtilmelidir. Bu, bir değeri "kopyalarsanız" aslında eski değeri yeniden kullandığınız ve referans sayısını (refcount) artırdığınız anlamına gelir. Yalnızca bir tür değişiklik yaptıktan sonra gerçek bir kopya ("çoğaltma" olarak adlandırılır) yapılır. Bkz Sen yalan ediliyoruz Bu konuyla ilgili daha kapsamlı bir giriş için.

PHP 5

Dahili dizi işaretçisi ve HashPointer

PHP 5'teki dizilerin, değişiklikleri düzgün şekilde destekleyen bir özel "dahili dizi işaretçisi" (IAP) vardır: Bir öğe kaldırıldığında, IAP'nin bu öğeye işaret edip etmediğini kontrol eder. Varsa, bunun yerine bir sonraki öğeye ilerletilir.

İken foreachtek IAP yoktur, ancak bir dizi çoklu parçası olabilir: İEN'nin yapmak kullanımını yapar ülkede ek bir komplikasyonudur foreachdöngüler:

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

Sadece bir dahili dizi işaretçisi ile iki eşzamanlı döngüyü desteklemek için foreach, aşağıdaki parlaklığı gerçekleştirir: Döngü gövdesi yürütülmeden önce, foreachbir işaretçiyi geçerli öğeye ve karma değerini her foreach için yedekler HashPointer. Döngü gövdesi çalıştıktan sonra, IAP yine de varsa bu öğeye geri ayarlanır. Ancak öğe kaldırıldıysa, yalnızca IAP'nin şu anda bulunduğu her yerde kullanırız. Bu şema çoğunlukla bir tür işler, ancak bunlardan bazılarını aşağıda göstereceğim çok tuhaf davranışlar var.

Dizi çoğaltma

IAP, bir dizinin görünür bir özelliğidir ( currentişlev ailesi aracılığıyla ortaya çıkar ), bu nedenle IAP sayısındaki değişiklikler yazma üzerine yazma semantiği altında değişiklikler olarak yapılır. Bu ne yazık ki, foreachçoğu durumda yinelediği diziyi çoğaltmaya zorlandığı anlamına gelir . Kesin koşullar:

  1. Dizi bir referans değil (is_ref = 0). Bir referans mesele buysa, edilir, sonra değişir sözde yaymak için onu yinelenmemelidir yüzden.
  2. Dizide refcount> 1 var. Eğer refcount1, daha sonra dizi paylaşılmayan ve doğrudan değiştirmek için özgürsünüz.

Dizi çoğaltılmazsa (is_ref = 0, refcount = 1), yalnızca dizisi refcountartırılır (*). Ek olarak, foreachreferans olarak kullanılırsa, (potansiyel olarak çoğaltılan) dizi referansa dönüştürülecektir.

Çoğaltmanın gerçekleştiği bir örnek olarak bu kodu düşünün:

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

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

Burada, $arrIAP değişikliklerinin $arrsızmasını önlemek için çoğaltılacaktır $outerArr. Yukarıdaki koşullar açısından, dizi bir referans değildir (is_ref = 0) ve iki yerde kullanılır (refcount = 2). Bu gereksinim talihsizdir ve en düşük uygulamanın bir yapaylığıdır (burada yineleme sırasında değişiklik yapma endişesi yoktur, bu yüzden ilk etapta IAP'yi kullanmamız gerekmez).

(*) refcountBurada artırılması zararsız gelebilir, ancak yazma üzerine kopyalama (COW) semantiğini ihlal eder: Bu, refcount = 2 dizisinin IAP'sini değiştireceğimiz anlamına gelir; COW, değişikliklerin yalnızca refcount = 1 değer. Bu ihlal, yinelenen dizideki IAP değişikliği gözlemlenebileceğinden (ancak bir COW normalde saydamken) kullanıcı tarafından görülebilir davranış değişikliği ile sonuçlanır - ancak yalnızca dizideki ilk IAP olmayan değişiklik yapılana kadar. Bunun yerine, üç "geçerli" seçenek a) her zaman çoğalmak, b) refcountartmaz ve böylece yinelenen dizinin döngüde keyfi olarak değiştirilmesine izin vermek veya c) IAP'yi hiç kullanmamak (PHP 7 çözüm).

Pozisyon ilerleme talimatı

Aşağıdaki kod örneklerini doğru bir şekilde anlamak için bilmeniz gereken son bir uygulama ayrıntısı vardır. Bazı veri yapıları arasında geçiş yapmanın "normal" yolu, sözde kodda şöyle görünür:

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

Ancak foreach, oldukça özel bir kar tanesi olarak, işleri biraz farklı yapmayı seçer:

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

Diğer bir deyişle, dizi işaretçisi döngü gövdesi çalışmadan önce ileriye doğru hareket ettirilir . Bu, döngü gövdesi eleman üzerinde çalışırken $i, IAP'nin zaten elemanda olduğu anlamına gelir $i+1. Bu kod örnekleri yineleme sırasında modifikasyonunu gösteren nedeni budur olacak hep bir sonraki eleman yerine geçerli bir.unset

Örnekler: Test örnekleriniz

Yukarıda açıklanan üç yön, size foreachuygulamanın kendine özgü ifadeleri hakkında tam bir izlenim vermelidir ve bazı örnekleri tartışmaya devam edebiliriz.

Test durumlarınızın davranışını bu noktada açıklamak kolaydır:

  • Test durumlarında 1 ve 2 $array, refcount = 1 ile başlar, bu nedenle aşağıdakiler tarafından çoğaltılmaz foreach: Yalnızca refcountartırılır. Döngü gövdesi daha sonra diziyi değiştirdiğinde (bu noktada refcount = 2 olan), çoğaltma o noktada gerçekleşir. Foreach, değiştirilmemiş bir kopyası üzerinde çalışmaya devam edecek $array.

  • Test durumu 3'te, bir kez daha dizi çoğaltılmaz, bu nedenle değişkenin foreachIAP'sini değiştirir $array. Yinelemenin sonunda, IAP NULL'dur (yani yineleme yapıldı), bu da eachgeri dönerek gösterir false.

  • Test durumları 4 ve 5'in her ikisi de eachve resetreferans fonksiyonlarıdır. $arrayBir sahiptir refcount=2o çoğaltılamaz zorundadır yüzden onlara geçirildiğinde. Bu foreachyüzden yine ayrı bir dizi üzerinde çalışacaktır.

Örnekler: currentin foreach'in etkileri

Çeşitli çoğaltma davranışlarını göstermenin iyi bir yolu, current()işlevin bir foreachdöngü içindeki davranışını gözlemlemektir . Bu örneği düşünün:

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

Burada current(), diziyi değiştirmese bile bir by-ref işlevi (aslında: prefer-ref) olduğunu bilmelisiniz . Tamamen nextref gibi olan diğer tüm işlevlerle güzel oynamak için olmalı . Tarafından referans geçen dizi ayrılacak sahiptir ve bu nedenle bu ifade eder $arrayve foreach-arrayfarklı olacaktır. Bunun 2yerine almanızın nedeni 1yukarıda da belirtilmiştir: kullanıcı kodunu çalıştırmadan önceforeach dizi işaretçisini ilerletir . Kod ilk öğede olsa bile, zaten işaretçiyi ikinciye ilerletmiştir.foreach

Şimdi küçük bir değişiklik yapalım:

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

Burada is_ref = 1 kasası var, bu yüzden dizi kopyalanmıyor (tıpkı yukarıdaki gibi). Ancak artık bir referans olduğu için, by-ref current()işlevine geçerken dizinin artık çoğaltılması gerekmez . Bu nedenle current()ve foreachaynı dizi üzerinde çalışır. foreachİşaretçiyi ilerletme biçimi nedeniyle hala tek tek davranışı görüyorsunuz .

By-ref yinelemesi yaparken aynı davranışı elde edersiniz:

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

Burada önemli olan, $arrayforeach'in referans ile tekrarlandığında bir is_ref = 1 yapmasıdır, bu yüzden temelde yukarıdaki ile aynı duruma sahipsiniz.

Başka bir küçük varyasyon, bu sefer diziyi başka bir değişkene atayacağız:

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

Burada $arraydöngü başlatıldığında ' in refcount değeri 2'dir, bu yüzden bir kez aslında çoğaltmayı önceden yapmak zorundayız. Böylece $array, foreach tarafından kullanılan dizi başlangıçtan tamamen ayrı olacaktır. Bu yüzden IAP'nin konumunu döngüden önceki herhangi bir yere alırsınız (bu durumda ilk konumdadır).

Örnekler: Yineleme sırasında değişiklik

Yineleme sırasındaki değişiklikleri hesaba katmaya çalışmak, tüm foreach sorunlarımızın ortaya çıktığı yerdir, bu nedenle bu durum için bazı örnekleri dikkate almaya hizmet eder.

Aynı dizi üzerinde bu iç içe geçmiş döngüleri düşünün (burada gerçekten aynı olduğundan emin olmak için by-ref yinelemesi kullanılır):

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)

Burada beklenen kısım, (1, 2)eleman 1kaldırıldığı için çıktıda eksik olmasıdır . Muhtemelen beklenmedik olan şey, dış halkanın ilk elemandan sonra durmasıdır. Neden?

Bunun nedeni yukarıda açıklanan iç içe döngü hack'idir: Döngü gövdesi çalışmadan önce, geçerli IAP konumu ve karması a HashPointer. Döngü gövdesinden sonra geri yüklenir, ancak yalnızca öğe hala mevcutsa, bunun yerine geçerli IAP konumu (her ne olursa olsun) kullanılır. Yukarıdaki örnekte durum tam olarak şu şekildedir: Dış halkanın mevcut elemanı kaldırılmıştır, bu nedenle zaten iç halka tarafından tamamlanmış olarak işaretlenmiş olan IAP'yi kullanır!

HashPointerYedekleme + geri yükleme mekanizmasının bir başka sonucu, reset()vb. Aracılığıyla IAP'deki değişikliklerin genellikle etkilememesidir foreach. Örneğin, aşağıdaki kod reset()hiç yokmuş gibi yürütülür :

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

Bunun nedeni, reset()IAP'yi geçici olarak değiştirirken, döngü gövdesinden sonra geçerli foreach öğesine geri yüklenmesidir. reset()Döngü üzerinde bir efekt yapmaya zorlamak için, geçerli öğeyi ek olarak kaldırmanız gerekir, böylece yedekleme / geri yükleme mekanizması başarısız olur:

$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

Ancak, bu örnekler hala aklı başında. Gerçek eğlence, HashPointergeri yüklemenin öğenin ve karmasının bir işaretçisi kullandığını hatırlarsanız, hala var olup olmadığını belirlemek için başlar. Ancak: Hash'lerin çarpışmaları var ve işaretçiler tekrar kullanılabilir! Bu, dikkatli bir dizi anahtar seçimi ile, foreachkaldırılan bir öğenin hala var olduğuna inanabileceğimiz için doğrudan ona atlayacağımız anlamına gelir. Bir örnek:

$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

Burada normalde çıktıyı 1, 1, 3, 4önceki kurallara göre beklemeliyiz . Ne 'FYFY'olduğu, kaldırılan öğeyle aynı karmaya sahip 'EzFY've ayırıcı, öğeyi saklamak için aynı bellek konumunu yeniden kullanıyor. Böylece foreach doğrudan yeni eklenen elemana atlar, böylece döngüyü kısa keser.

Döngü sırasında yinelenen varlığı değiştirme

Bahsetmek istediğim son bir garip durum, PHP döngü sırasında yinelenen varlık yerine izin verir olmasıdır. Böylece bir dizide yinelemeyi başlatabilir ve daha sonra bu diziyi yarıya kadar başka bir diziyle değiştirebilirsiniz. Veya bir dizi üzerinde yinelemeyi başlatın ve bir nesneyle değiştirin:

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

Bu durumda görebileceğiniz gibi PHP, ikame gerçekleştikten sonra diğer varlığı en baştan tekrarlamaya başlayacaktır.

PHP 7

Hashtable yineleyicileri

Hala hatırlıyorsanız, dizi yinelemesinde ana sorun, öğelerin orta yinelemenin kaldırılmasıyla nasıl başa çıkılacağıydı. Bir dizi gösterici aynı anda birden çok foreach döngüleri desteklemek için uzatılmış zorunda PHP 5, biraz suboptimal bu amaç için tek bir iç dizi gösterici (IAP), kullanılan ve etkileşimi reset()bunun üstüne alır.

PHP 7 farklı bir yaklaşım kullanır, yani keyfi olarak harici, güvenli karma yineleyiciler oluşturmayı destekler. Bu yineleyicilerin diziye kaydedilmesi gerekir; bu noktadan sonra, IAP ile aynı semantiğe sahiptirler: Bir dizi öğesi kaldırılırsa, o öğeyi işaret eden tüm karma yineleyiciler bir sonraki öğeye ilerletilir.

Bu araçlar foreachartık IAP kullanacak hiç . foreachDöngü sonuçlarına kesinlikle hiçbir etkisi olacaktır current()vb ve kendi davranış gibi işlevler etkilenir asla reset()vs.

Dizi çoğaltma

PHP 5 ve PHP 7 arasındaki bir diğer önemli değişiklik, dizi çoğaltma ile ilgilidir. Artık IAP artık kullanılmadığına göre, by-value dizi yinelemesi her durumda yalnızca bir refcountartış (diziyi çoğaltmak yerine) yapacaktır . Dizi foreachdöngü sırasında değiştirilirse , bu noktada bir çoğaltma gerçekleşir (yazma üzerine kopyalamaya göre) ve foreacheski dizi üzerinde çalışmaya devam eder.

Çoğu durumda, bu değişiklik şeffaftır ve daha iyi performanstan başka bir etkisi yoktur. Bununla birlikte, farklı davranışlarla sonuçlanan bir durum vardır, yani dizinin önceden bir referans olduğu durum:

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

Daha önce referans dizilerinin by-value iterasyonu özel durumlardı. Bu durumda çoğaltma gerçekleşmediğinden, yineleme sırasında dizideki tüm değişiklikler döngü tarafından yansıtılacaktır. PHP 7'de bu özel durum gitti: Bir dizinin by-value yinelemesi her zaman orijinal elemanlar üzerinde çalışmaya devam edecek ve döngü sırasında yapılan değişiklikleri göz ardı edecektir.

Bu, elbette, referans referans yinelemesi için geçerli değildir. Referansı yinelerseniz, tüm değişiklikler döngü tarafından yansıtılır. İlginç bir şekilde, aynı şey düz nesnelerin by-value yinelemesi için de geçerlidir:

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

Bu, nesnelerin by-pass semantiklerini yansıtır (yani, by-value bağlamlarında bile referansa benzer davranırlar).

Örnekler

Test örneklerinizden başlayarak birkaç örneği ele alalım:

  • Test durumları 1 ve 2 aynı çıktıyı korur: By-value dizi yinelemesi her zaman orijinal öğeler üzerinde çalışmaya devam eder. (Bu durumda, çift refcountingve yineleme davranışı PHP 5 ve PHP 7 arasında tam olarak aynıdır).

  • Test durumu 3 değişiklikleri: Foreachartık IAP'yi kullanmaz, bu nedenle each()döngüden etkilenmez. Öncesi ve sonrası aynı çıkışa sahip olacaktır.

  • Test senaryoları 4 ve 5 aynı kalır: each()ve reset()IAP'yi değiştirmeden önce diziyi çoğaltır, foreachyine de orijinal diziyi kullanır. (Dizi paylaşılsa bile IAP değişikliğinin önemli olduğu anlamına gelmez.)

İkinci grup örnekler current()farklı reference/refcountingkonfigürasyonlar altındaki davranışlar ile ilgilidir . current()Döngüden tamamen etkilenmediği için bu artık bir anlam ifade etmiyor, bu nedenle dönüş değeri her zaman aynı kalıyor.

Bununla birlikte, yineleme sırasında değişiklikler düşünülürken bazı ilginç değişiklikler elde ediyoruz. Umarım yeni davranışı daha sağlıklı bulursun. İlk örnek:

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

Gördüğünüz gibi, dış döngü artık ilk yinelemeden sonra durmuyor. Bunun nedeni, her iki döngünün artık tamamen ayrı hashtable yineleyicilere sahip olması ve artık her iki döngünün paylaşılan bir IAP aracılığıyla çapraz kontaminasyonu olmamasıdır.

Şimdi düzeltilen bir başka garip kenar durumu, aynı karmaya sahip öğeleri kaldırdığınızda ve eklediğinizde elde ettiğiniz garip etkidir:

$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

Daha önce HashPointer geri yükleme mekanizması doğrudan yeni öğeye atladı, çünkü kaldırılan öğeyle aynı gibi görünüyordu (çarpışma hash ve pointer nedeniyle). Artık hiçbir şey için öğe karmasına güvenmediğimizden, bu artık bir sorun değil.


4
@Baba Öyle. Bir işleve $foo = $array
iletmek

32

1
Küçük düzeltme: Bucket adını verdiğiniz şey, normalde bir hashtable içinde Bucket adı verilen şey değildir. Normalde Bucket, aynı hash% boyutuna sahip bir girdi kümesidir. Normalde giriş olarak adlandırılanlar için kullanıyorsunuz. Bağlantılı liste bölümlerde değil girişlerde.
unbeli

12
@unbeli PHP tarafından dahili olarak kullanılan terminolojiyi kullanıyorum. BucketS iki kat bağlantılı karma çarpışmalar listesi ve bir dizi iki kat bağlantılı liste de kısmen bir parçasıdır;)
Nikic

4
Harika anwser. Sanırım demek istediğin bir yer iterate($outerArr);değil iterate($arr);.
niahoo

116

Örnek 3'te diziyi değiştirmezsiniz. Diğer tüm örneklerde içeriği veya dahili dizi işaretçisini değiştirirsiniz. Bu, atama operatörünün anlambilimi nedeniyle PHP dizileri söz konusu olduğunda önemlidir .

PHP'deki diziler için atama operatörü daha çok tembel bir klon gibi çalışır. Bir diziyi içeren bir değişkene bir değişken atamak, çoğu dilden farklı olarak diziyi klonlar. Ancak, gerçek klonlama gerekmedikçe yapılmayacaktır. Bu, klonun yalnızca değişkenlerden herhangi biri değiştirildiğinde (yazma üzerine kopyalama) gerçekleşeceği anlamına gelir.

İşte bir örnek:

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

Test durumlarınıza geri döndüğünüzde foreach, diziye referansla bir çeşit yineleyici oluşturduğunu kolayca hayal edebilirsiniz . Bu referans, örneğimdeki değişken gibi çalışır $b. Ancak, yineleyici referans ile birlikte sadece döngü sırasında canlı ve daha sonra, her ikisi de atılır. Şimdi, bu ekstra referans canlıyken, 3 dışında tüm durumlarda dizinin döngü sırasında değiştirildiğini görebilirsiniz. Bu bir klonu tetikler ve bu da burada neler olduğunu açıklar!

İşte bu yazma-üzerine yazma davranışı başka bir yan etkisi için mükemmel bir makale: PHP üçlü operatör: hızlı ya da değil?


doğru gibi görünüyor, bunu gösteren bir örnek yaptım: codepad.org/OCjtvu8r örneğinizden bir fark - değeri değiştirirseniz, yalnızca değişiklik anahtarları varsa kopyalanmaz.
zb '

Bu aslında yukarıdaki tüm davranışları açıklar each()ve ilk test vakasının sonunda çağrılarak güzel bir şekilde gösterilebilir , burada dizi sırasında değiştirildiği için orijinal dizinin dizi işaretçisinin ikinci öğeyi gösterdiğini görürüz . ilk yineleme. Bu da foreachben beklemiyordum döngü kod bloğu yürütmeden önce dizi işaretçisi hareket gösterir - ben sonunda bunu yapacağını düşünürdüm. Çok teşekkürler, bu benim için güzelce temizler.
DaveRandom

49

Çalışırken dikkat edilmesi gereken bazı noktalar foreach():

a) orijinal dizinin beklenen kopyasıforeach üzerinde çalışır . Her Not / Kullanıcı yorumu için a oluşturulmadıkça veya oluşturulmadıkça PAYLAŞILACAK veri depolama anlamına gelir .foreach()prospected copy

b) Beklenen bir kopyayı ne tetikler ? Potansiyel bir kopya copy-on-write, yani geçirilen bir dizi foreach()değiştirildiğinde, orijinal dizinin bir kopyası oluşturulur.

c) Orijinal dizi ve foreach()yineleyici DISTINCT SENTINEL VARIABLES, biri orijinal dizi ve diğeri için foreach; aşağıdaki test koduna bakın. SPL , Yineleyiciler ve Dizi Yineleyici .

Yığın taşması sorusu PHP'de bir 'foreach' döngüsünde değerin sıfırlandığından nasıl emin olunur? sorunuzun vakalarını (3,4,5) ele alır.

Aşağıdaki örnek, Şekil her () ve reset () etkilemediğini SENTINELdeğişkenler (for example, the current index variable)arasında foreach()yineleyici.

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

Çıktı:

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

2
Cevabınız tam olarak doğru değil. foreachdizinin olası bir kopyasında çalışır, ancak gerekmedikçe gerçek kopyayı oluşturmaz.
linepogl

bu potansiyel kopyanın kodla nasıl ve ne zaman oluşturulduğunu göstermek ister misiniz? foreachKodum dizi% 100 zaman kopyalamak gösterir . Bilmek için can atıyorum. Yorumlarınız için teşekkürler
sakhunzai

Bir dizi kopyalamanın maliyeti çok fazladır. forVeya öğelerini kullanarak 100000 öğeli bir diziyi yinelemek için geçen süreyi saymayı deneyin foreach. İkisi arasında önemli bir fark görmeyeceksiniz, çünkü gerçek bir kopya yer almıyor.
linepogl

Sonra SHARED data storagekadar ya da sürece ayrılmış olduğunu varsayalım copy-on-write, ama (benim kod snippet'ten) her zaman SENTINEL variablesbiri için original arrayve diğeri için bir dizi olacak açıktır foreach. Mantıklı teşekkürler
sakhunzai

1
Evet, bu "beklenen" kopya yani "potansiyel" kopyadır. Önerdiğiniz gibi
korunmuyor

33

PHP 7 İÇİN NOT

Bu cevabı biraz popülerlik kazandığı için güncellemek için: Bu cevap PHP 7'den itibaren geçerli değildir. " Geriye dönük uyumsuz değişiklikler " bölümünde açıklandığı gibi, PHP 7 foreach dizinin kopyası üzerinde çalışır, bu nedenle dizinin kendisindeki değişiklikler foreach döngüsüne yansıtılmaz. Bağlantıda daha fazla ayrıntı.

Açıklama ( php.net'ten alıntı ):

İlk form dizi_ifadesi tarafından verilen dizinin üzerinden geçer. Her bir yinelemede, geçerli öğenin değeri $ değerine atanır ve dahili dizi işaretçisi bir ileri ilerler (böylece bir sonraki yinelemede bir sonraki öğeye bakacaksınız).

Bu nedenle, ilk örneğinizde dizide yalnızca bir öğeniz vardır ve işaretçi taşındığında bir sonraki öğe mevcut değildir, bu nedenle yeni bir öğe ekledikten sonra zaten son öğe olarak "buna karar verdiğinden" yeni bir öğe ekledikten sonra sona erer.

İkinci örneğinizde, iki öğeyle başlarsınız ve foreach döngüsü son öğede değildir, bu nedenle diziyi sonraki yinelemede değerlendirir ve böylece dizide yeni bir öğe olduğunu fark eder.

Bunun tüm belgelerin açıklamasının her yinelemede bir parçası olduğuna inanıyorum , bu muhtemelen foreachkodu çağırmadan önce tüm mantığı yapar {}.

Test durumu

Bunu çalıştırırsanız:

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

Bu çıktıyı alacaksınız:

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

Bu, değişikliği kabul ettiği ve "zamanında" değiştirildiği için geçtiği anlamına gelir. Ancak bunu yaparsanız:

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

Alacaksın:

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

Bu, dizinin değiştirildiği anlamına gelir, ancak dizinin foreachson öğesindeyken diziyi değiştirdiğimizden, artık döngü oluşturmamaya "karar verdi" ve yeni öğe eklesek bile, bunu "çok geç" ekledik ve üzerinden dönmedi.

Ayrıntılı açıklama PHP 'foreach' aslında nasıl çalışır? bu da bu davranışın ardındaki içselleri açıklar.


7
Cevabın geri kalanını okudun mu? Foreach'in , içindeki kodu çalıştırmadan önce başka bir zamana dönüp dönmeyeceğine karar vermesi mükemmel bir mantıklı .
dkasipovic

2
Hayır, dizi değiştirildi, ancak "çok geç" çünkü foreach zaten son öğede olduğunu (yinelemenin başlangıcında olduğunu) ve artık döngü yapmayacağını düşünüyor. İkinci örnekte, yinelemenin başlangıcındaki son öğe değildir ve bir sonraki yinelemenin başlangıcında tekrar değerlendirilir. Bir test davası hazırlamaya çalışıyorum.
dkasipovic

1
@AlmaDo lxr.php.net/xref/PHP_TRUNK/Zend/zend_vm_def.h#4509 bakın Yinelendiğinde her zaman bir sonraki işaretçiye ayarlanır. Bu nedenle, son yinelemeye ulaştığında, bitmiş olarak işaretlenir (NULL işaretçisi aracılığıyla). Son yinelemede bir anahtar eklediğinizde, foreach bunu fark etmeyecektir.
bwoebi

1
@DKasipovic no. Orada tam ve net bir açıklama yok (en azından şimdilik - yanlış olabilirim)
Alma Do

4
Aslında @AmaDo'nun kendi mantığını anlamada bir kusuru var gibi görünüyor ... Cevabınız iyi.
bwoebi

15

PHP kılavuzu tarafından sağlanan belgelere göre.

Her bir yinelemede, geçerli öğenin değeri $ v değerine atanır ve dahili
dizi işaretçisi bir ileri gider (bir sonraki yinelemede bir sonraki öğeye bakacaksınız).

İlk örneğinize göre:

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

$arraysadece tek bir öğeye sahip, bu yüzden foreach yürütme başına 1 atamak $vve işaretçiyi taşımak için başka bir öğe yok

Ancak ikinci örneğinizde:

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

$arrayiki öğe var, bu yüzden şimdi $ dizisi sıfır indekslerini değerlendirir ve işaretçiyi birer birer taşır. Döngünün ilk yinelemesi için $array['baz']=3;referans olarak geçiş olarak eklendi .


13

Büyük soru, çünkü birçok geliştirici, hatta deneyimli olanlar, PHP'nin foreach döngülerindeki dizileri işleme biçimiyle karıştırılır. Standart foreach döngüsünde PHP, döngüde kullanılan dizinin bir kopyasını oluşturur. Kopya, döngü bittikten hemen sonra atılır. Bu, basit bir foreach döngüsünün işleminde saydamdır. Örneğin:

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

Bu çıktılar:

apple
banana
coconut

Böylece, orijinal diziye döngü içinde veya döngü bittikten sonra başvurulmadığından kopya oluşturulur, ancak geliştirici fark etmez. Ancak, bir döngüdeki öğeleri değiştirmeye çalıştığınızda, işiniz bittiğinde değiştirilmemiş olduklarını görürsünüz:

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

print_r($set);

Bu çıktılar:

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

Orijinal öğedeki herhangi bir değişiklik bildiri olamaz, aslında $ öğesine açıkça bir değer atamış olsanız bile, orijinalde herhangi bir değişiklik olmaz. Bunun nedeni, üzerinde çalışılan $ setinin kopyasında göründüğü gibi $ öğesi üzerinde çalışmanızdır. $ İtem'i referans olarak aşağıdaki gibi yakalayarak geçersiz kılabilirsiniz:

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

Bu çıktılar:

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

Dolayısıyla, $ item referansla çalıştırıldığında, $ item üzerinde yapılan değişiklikler orijinal $ setinin üyelerine yapılır. $ Öğesinin başvuru ile kullanılması PHP'nin dizi kopyasını oluşturmasını da engeller. Bunu test etmek için, önce kopyayı gösteren hızlı bir komut dosyası göstereceğiz:

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

Bu çıktılar:

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

Örnekte görüldüğü gibi, PHP $ set kopyaladı ve bunu döngü için kullandı, ancak $ set döngü içinde kullanıldığında, PHP değişkenleri kopyalanan diziye değil, orijinal diziye ekledi. Temel olarak, PHP yalnızca döngünün yürütülmesi ve $ öğesinin atanması için kopyalanan diziyi kullanır. Bu nedenle, yukarıdaki döngü yalnızca 3 kez yürütülür ve orijinal $ kümesinin sonuna başka bir değer eklediğinde, orijinal $ kümesini 6 öğeyle bırakır, ancak asla sonsuz döngüye girmez.

Ancak, daha önce de belirttiğim gibi, referans olarak $ item kullansaydık? Yukarıdaki teste tek bir karakter eklendi:

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

Sonsuz bir döngü oluşturur. Bunun sonsuz bir döngü olduğunu unutmayın, komut dosyasını kendiniz öldürmeniz veya işletim sisteminizin belleğinin bitmesini beklemeniz gerekir. PHP'nin belleği çok hızlı tükenmesi için betiğime aşağıdaki satırı ekledim, bu sonsuz döngü testlerini çalıştırıyorsanız aynı şeyi yapmanızı öneririm:

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

Sonsuz döngü içeren bu önceki örnekte, PHP'nin döngü yapılacak dizinin bir kopyasını oluşturmak için neden yazıldığını görüyoruz. Bir kopya yalnızca döngü yapısının kendi yapısı tarafından oluşturulduğunda ve kullanıldığında, dizi, döngü yürütülürken statik kalır, böylece hiçbir zaman sorunla karşılaşmazsınız.


7

PHP foreach döngüsü Indexed arrays, Associative arraysve ile birlikte kullanılabilir Object public variables.

Foreach döngüsünde php'nin yaptığı ilk şey, yinelenecek dizinin bir kopyasını oluşturmasıdır. PHP daha sonra copyorijinal diziden ziyade dizinin bu yenisini yineler . Bu, aşağıdaki örnekte gösterilmiştir:

<?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).

Bunun yanı sıra, php de kullanıma izin verir iterated values as a reference to the original array value. Bu aşağıda gösterilmiştir:

<?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

Not:original array indexes Olarak kullanılmasına izin vermez references.

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


1
Object public variablesyanlış veya en iyi yanıltıcıdır. Bir dizideki bir nesneyi doğru arabirim (örneğin, Gezilebilir) olmadan kullanamazsınız ve bunu foreach((array)$obj ...yaptığınızda aslında basit bir dizi ile çalışırsınız, artık bir nesne ile değil.
Christian
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.