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


Bunu ne olduğunu bildiğimi söyleyerek önek vereyim foreach , yapar ve nasıl kullanılır. Bu soru, kaputun altında nasıl çalıştığıyla ilgilidir ve "Bir dizinin nasıl olduğuyla ilgili bu dizinin" foreach".


Uzun süredir bunu kabul ettim. foreach dizinin kendisi ile çalıştı. Daha sonra, bir kopya dizinin ve bunun hikayenin sonu olduğunu varsaydım. Ama son zamanlarda konuyla ilgili bir tartışmaya girdim ve küçük bir deneyden sonra bunun aslında% 100 doğru olmadığını tespit etti.

Ne demek istediğimi göstereyim. Aşağıdaki test senaryoları için aşağıdaki dizi ile çalışacağız:

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

Test davası 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 açıkça kaynak dizisi ile doğrudan çalışmadığımızı gösterir - aksi takdirde döngü sonsuza dek devam eder, çünkü döngü sırasında öğeleri sürekli olarak diziye aktarırız. Ama durumun böyle olduğundan emin olmak için:

Test durumu 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 destekliyor, döngü sırasında kaynak dizinin bir kopyasıyla çalışıyoruz, aksi takdirde döngü sırasında değiştirilmiş değerleri göreceğiz. Fakat...

Eğer bakarsak Manuel, bu ifadeyi buluruz:

Foreach ilk çalıştırmaya başladığında, dahili dizi işaretçisi otomatik olarak dizinin ilk elemanına sıfırlanır.

Doğru ... bu önermek gibi görünüyor foreach Kaynak dizinin dizi işaretçisine güvenir. Ama biz sadece kaynak dizisiyle çalışma, sağ? Tamamen değil.

Test çantası 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, kaynak dizisiyle doğrudan çalışmadığımız gerçeğine rağmen, doğrudan kaynak dizisi işaretçisi ile çalışıyoruz - işaretçinin, döngü sonunda dizinin sonunda olması gerçeği bunu gösteriyor. Bunun dışında doğru olamaz - eğer öyleyse test vakası 1 Sonsuza kadar döngü olur.

PHP kılavuzu ayrıca şunları belirtir:

Foreach, döngü içinde değiştirerek iç dizi işaretçisine dayanıyor gibi beklenmedik davranışlara yol açabilir.

Peki, "beklenmedik davranış" ın ne olduğunu öğrenelim (teknik olarak, ne beklediğimi bilmediğimden beri herhangi bir davranış beklenmedik).

Test durumu 4:

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

/* Output: 1 2 3 4 5 */

Test çantası 5:

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

/* Output: 1 2 3 4 5 */

... beklenmedik bir şey değil, aslında "kaynak kopya" teorisini destekliyor gibi görünüyor.


Soru

Burada neler oluyor? Benim C-fu, PHP kaynak koduna bakarak, doğru bir sonuca ulaşabilmem için yeterince iyi değil, birisi benim için İngilizce'ye çevirebilseydim bunu takdir ediyorum.

Bana öyle geliyor ki foreach ile çalışır kopya dizisi, ancak kaynak dizinin dizi işaretçisini döngüden sonra dizinin sonuna ayarlar.

  • Bu doğru ve tüm hikaye mi?
  • Değilse, gerçekten ne yapıyor?
  • Dizi işaretçisini ayarlayan işlevlerin kullanıldığı herhangi bir durum var mı (each(), reset() et al.) sırasında foreach Döngünün sonucunu etkileyebilir mi?

1644
2018-04-07 19:33


Menşei


@DaveRandom Bir var php-internals Bu etiketi muhtemelen gitmeli, ama değiştirecek diğer 5 etiketin hangisi olduğuna karar vermek için size bırakacağım. - Michael Berkowski
Silmeksizin COW gibi görünüyor - zb'
İlk başta düşündüm »Allah, başka bir yeni soru. Dokümanları oku… hm, açıkça tanımlanmamış davranış «. Sonra tam soruyu okudum ve şunu söylemeliyim: Bunu sevdim. Bunun için epeyce çaba harcadınız ve tüm test tüplerini yazdınız. ps. 4 ve 5'in aynısı mı? - knittl
Sadece dizi işaretçisinin dokunduğunun anlamlandırmasıyla ilgili bir düşünce: PHP, orijinal dizinin iç dizi işaretçisini, kopyayla birlikte sıfırlamak ve taşımak zorundadır, çünkü kullanıcı geçerli değere bir başvuru isteyebilir.foreach ($array as &$value)) - PHP aslında bir kopya üzerinde yineleme olsa bile orijinal dizideki geçerli konumu bilmesi gerekiyor. - Niko
@Sean: IMHO, PHP belgeleri çekirdek dil özelliklerinin nüanslarını tanımlamakta oldukça kötüydü. Ama belki de, o kadar çok ad hocalı özel durum dilin içinde pişiriliyor ... - Oliver Charlesworth


Cevaplar:


foreach üç farklı çeşit değer üzerinde yinelemeyi destekler:

Aşağıda, iterasyonun farklı durumlarda nasıl çalıştığını tam olarak açıklamaya çalışacağım. Şimdiye kadar en basit durum Traversable nesneler, bunlar için foreach Aslında bu satırlar boyunca kod için sadece sözdizimi şekerdir:

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çekte yalnızca yansıtma yapan dahili bir API kullanılarak gerçek yöntem çağrıları önlenir. Iterator C seviyesinde arayüz.

Dizilerin ve düz nesnelerin yinelenmesi önemli ölçüde daha karmaşıktır. Her şeyden önce, PHP "dizilerinde" gerçekten sipariş edilen sözlükler olduğu ve bu sıraya göre hareket ettirileceği not edilmelidir. sort). Bu, anahtarların doğal düzeninin (diğer dillerdeki listelerin çoğunlukla nasıl çalıştığı) ya da hiç tanımlanmamış bir düzenin (diğer dillerdeki sözlükler çoğunlukla işe yarar) yinelenmesine karşıdır.

Aynı şey nesneler için de geçerlidir, çünkü nesne özellikleri, değerlerine değerlerin eşlenmesini sağlayan bir başka (sıralı) sözlük ve bazı görünürlük işleme olarak görülebilir. Vakaların çoğunda nesne özellikleri aslında bu oldukça verimsiz bir şekilde saklanmaz. Ancak, bir nesne üzerinde yinelemeye başlarsanız, normalde kullanılan paketlenmiş temsil, gerçek bir sözlüğe dönüştürülür. Bu noktada, düz nesnelerin yinelemesi, dizilerin tekrarlanmasına çok benzer hale gelir (bu yüzden burada çok fazla basit nesne yinelemeyi tartışmayacağım).

Çok uzak çok iyi. Bir sözlük üzerinde yineleme yapmak çok zor olamaz, değil mi? Sorun, yineleme sırasında bir dizinin / nesnenin değişebileceğini fark ettiğinizde başlar. Bunun olabileceği çeşitli yollar var:

  • Kullanarak referans ile yineleyin foreach ($arr as &$v) sonra $arr referansa çevrilir ve yineleme sırasında bunu değiştirebilirsiniz.
  • PHP 5'te, aynı değerde yineleme yapsanız bile aynı şey geçerlidir, ancak dizi önceden bir referanstır: $ref =& $arr; foreach ($ref as $v)
  • Nesneler, pratik amaçlar için, referanslar gibi davrandıkları anlamına gelen semantikleri geçerek, by-handle'a sahiptir. Böylece nesneler yineleme sırasında her zaman değiştirilebilir.

Yineleme sırasında değişikliklere izin verme sorunu, şu anda üzerinde bulunduğunuz öğenin kaldırıldığı durumdur. Şu anda hangi dizi elemanın bulunduğunu takip etmek için bir işaretçi kullandığınızı varsayalım. Bu eleman şimdi serbest bırakılırsa, sarkan bir işaretçiyle (genellikle bir segfault ile sonuçlanır) kalırsınız.

Bu sorunu çözmenin farklı yolları vardır. PHP 5 ve PHP 7, bu bağlamda önemli ölçüde farklıdır ve aşağıdaki iki davranışı tanımlayacağım. Özetle, PHP 5'in yaklaşımının oldukça aptalca olduğu ve her türlü garip durumlarla ilgili sorunlara yol açtığı, buna karşılık PHP 7'nin daha fazla yaklaşan yaklaşımının daha öngörülebilir ve tutarlı davranışlara yol açtığıdır.

Son bir ön hazırlık olarak, PHP'nin belleği yönetmek için referans sayımını ve yazma üzerine yazmayı kullandığını belirtmek gerekir. Bu, bir değeri "kopyalarsanız" aslında eski değeri yeniden kullanmanız ve referans sayısını (refcount) artırmanız anlamına gelir. Sadece bir kez değişiklik yaptığınızda gerçek bir kopya ("çoğaltma" olarak adlandırılır) yapılacaktır. Görmek Yalan söylüyorsun Bu konu hakkında daha kapsamlı bir giriş için.

PHP 5

Dahili dizi işaretçisi ve HashPointer

PHP 5 dizilerinde, modifikasyonları doğru bir şekilde destekleyen bir "dahili dizi işaretçisi" (IAP) bulunur: Bir eleman her kaldırıldığında, IAP'nin bu öğeye işaret edip etmediğini kontrol eder. Bunu yaparsa, bunun yerine bir sonraki öğeye ilerler.

Foreach IAP'yi kullanırken, ek bir sorun vardır: Yalnızca bir IAP vardır, ancak bir dizi çoklu foreach döngülerinin bir parçası olabilir:

// 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 iç dizi işaretçisi ile iki eşzamanlı döngüyü desteklemek için, foreach aşağıdaki schenanigans'ı gerçekleştirir: Döngü gövdesi yürütülmeden önce, foreach geçerli öğeye bir işaretçiyi ve fors for a foreach'a yedekleyecektir. HashPointer. Döngü gövdesi çalıştıktan sonra, hala mevcutsa IAP bu öğeye geri getirilecektir. Bununla birlikte, öğe kaldırılmışsa, yalnızca IAP'nin bulunduğu yerde kullanırız. Bu şema çoğunlukla-kimi-sortof çalışır, ama içinden alabileceğimiz bir sürü garip davranış var, bunlardan bazıları aşağıda göstereceğim.

Dizi çoğaltması

IAP bir dizinin görünür bir özelliğidir ( current işlevler ailesi), bu şekilde yazılanlar, Write-on-write semantics altında değişiklik olarak sayılır. Bu maalesef foreach'ın birçok durumda tekrarladığı diziyi çoğaltmaya zorladığı anlamına gelir. Kesin koşullar şunlardır:

  1. Dizi bir referans değil (is_ref = 0). Bir referans ise, o zaman değişiklikler sözde yayılmak, bu nedenle çoğaltılmamalıdır.
  2. Dizi,> 1 hesabına sahip. Yeniden sayım 1 ise, dizi paylaşılmaz ve doğrudan değiştirebiliriz.

Dizi çoğaltılmamışsa (is_ref = 0, refcount = 1), o zaman sadece refcount değeri artar (*). Ayrıca, eğer referans ile foreach kullanılırsa, (potansiyel olarak çoğaltılmış) dizi bir referansa dönüştürülecektir.

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

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

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

İşte, $arr IAP değişikliklerini önlemek için çoğaltılacak $arr sızmaktan $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 gereklilik talihsiz ve yetersiz uygulamanın bir eseridir (burada yineleme sırasında değişiklik yapılmasına dair bir endişe yoktur, bu yüzden UİS'yi gerçekten ilk etapta kullanmamız gerekmez).

(*) Buradaki refcount değerini artırma zararsızdır, ancak yazma-yazma (COW) semantiğini ihlal eder: Bu, refcount = 2 dizisinin IAP'sini değiştireceğimiz anlamına gelirken, COW değişikliklerin yalnızca refcount'ta gerçekleştirilebileceğini belirtir. = 1 değer. Bu ihlal, kullanıcı tarafından görülebilen davranış değişikliğine neden olur (COW normalde şeffaftır), çünkü yinelenen dizideki IAP değişkeni gözlenebilir olur - ancak yalnızca dizideki ilk IAP dışı değişikliğe kadar. Bunun yerine, üç "geçerli" seçeneğin a) her zaman çoğaltılması, b) geri sayımı arttırmadığı ve böylece yinelenen dizinin döngüde keyfi olarak değiştirilmesine izin vermeyeceği veya c) UİS'yi hiç kullanmadığı ( PHP 7 çözümü).

Konum ilerletme sırası

Aşağıdaki kod örneklerini doğru olarak anlamak için bilmeniz gereken son bir uygulama detayı vardır. Bazı veri yapısı boyunca "normal" bir şekilde döngü yapmak, sözde kodda böyle bir şeye benzeyecektir:

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

ancak foreacholdukça özel bir kar tanesi olmak, biraz farklı şeyler yapmayı seçer:

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

Yani, dizi işaretçisi zaten ileriye taşındı önce döngü gövdesi çalışır. Bunun anlamı, döngü gövdesi eleman üzerinde çalışırken $i, IAP zaten öğede $i+1. Yineleme sırasında değişiklik gösteren kod örneklerinin neden her zaman Sonraki eleman, geçerli olandan çok.

Örnekler: Test durumlarınız

Yukarıda açıklanan üç yön, size foreach uygulamasının özdeyişlerine dair tam bir izlenim sunmalı ve bazı örnekleri tartışmaya devam edebiliriz.

Test durumlarınızın davranışının bu noktada açıklanması basittir:

  • 1 ve 2 numaralı testlerde $array refcount = 1 ile başlıyor, bu yüzden foreach tarafından çoğaltılmayacak: Yalnızca refcount artırıldı. Döngü birimi sonradan diziyi değiştirdiğinde (bu noktada refcount = 2), bu noktada çoğaltma gerçekleşecektir. Foreach, değiştirilmemiş bir kopyası üzerinde çalışmaya devam edecek $array.

  • Test durumunda 3, bir kez daha dizi çoğaltılmamıştır, bu nedenle foreach IAP'yi değiştirecektir. $array değişken. Yinelemenin sonunda IAP NULL (iterasyon anlamına gelir), each dönerek belirtir false.

  • 4 ve 5 numaralı test durumlarında each ve reset referans fonksiyonlarıdır. $array bir var refcount=2 Onlara geçtiğinde, çoğaltılması gerekir. Gibi foreach Yine ayrı bir dizi üzerinde çalışacak.

Örnekler: Etkileri current foreach içinde

Çeşitli çoğaltma davranışlarını göstermenin iyi bir yolu, davranışını gözlemlemektir. current() bir foreach döngüsünde çalışır. Bu örneği düşünün:

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

İşte bunu bilmelisin current() diziyi değiştirmese de bir ref fonksiyonu (aslında: ref-ref) 'dir. Bu gibi diğer tüm fonksiyonları ile güzel oynamak için olmalı next hepsi by-ref. Referans referansı, dizinin ayrılması gerektiğini ve dolayısıyla $array ve foreach dizisi farklı olacak. Almanın nedeni 2 yerine 1 yukarıda da belirtilmiştir: foreach dizi işaretçisini ilerletir önce kullanıcı kodunu çalıştırdıktan sonra değil. Bu nedenle, kod ilk öğede olsa bile foreach zaten işaretçiyi ikinciye ilerletti.

Şimdi küçük bir değişiklik yapmayı deneyelim:

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

Burada is_ref = 1 durumumuz var, bu yüzden dizi kopyalanmıyor (yukarıdaki gibi). Ama şimdi bu bir referans, dizinin artık by-ref'ye geçerken kopyalanması gerekmiyor. current() işlevi. Böylece current() ve foreach aynı dizide çalışır. Yoldan dolayı hala bire bir davranış görüyorsunuz. foreachişaretçiyi ilerletir.

Yeniden-yineleme yaparken aynı davranışı alırsınız:

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

Burada önemli olan foreach yapacak $array referans ile yinelenen bir is_ref = 1, temelde yukarıdakiyle aynı durumunuz var.

Başka bir küçük değişiklik, 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 */

İşte refcount $array Döngü başlatıldığında 2, bu yüzden bir kez biz tekrar çoğaltma yapmak zorundayız. Böylece $array foreach tarafından kullanılan dizi başlangıçtan tamamen ayrı olacaktır. İşte bu yüzden, döngüden önceki nerede olursa olsun, IAP'nin konumunu elde edersiniz (bu durumda ilk pozisyondaydı).

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

Yineleme sırasında modifikasyonları hesaba katmak, tüm foreach sorunlarımızın kaynaklandığı yerdir, bu yüzden bu vaka için bazı örnekleri dikkate almaya yarar.

Aynı dizideki bu iç içe geçmiş döngüleri göz önünde bulundurun (burada aynı yinelemenin aynı olduğundan emin olmak için 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)

Beklenen kısım şu ki (1, 2) çıktıdan eksik, çünkü eleman 1 kaldırıldı. Muhtemelen beklenmeyen şey, dış döngü ilk elemandan sonra durmasıdır. Neden?

Bunun arkasındaki neden, yukarıda açıklanan iç içe döngü hack'idir: Döngü gövdesi çalışmaya başlamadan önce, geçerli IAP konumu ve karması bir HashPointer. Döngü gövdesinden sonra geri yüklenir, ancak sadece eleman hala mevcutsa, bunun yerine mevcut UDP konumu (ne olursa olsun) kullanılır. Yukarıdaki örnekte bu tam olarak şu şekildedir: Dış çevrimin mevcut elemanı kaldırılmıştır, bu yüzden iç döngü tarafından bittiği haliyle işaretlenmiş olan IAP'yi kullanacaktır!

Başka bir sonucu HashPointer yedekleme + geri yükleme mekanizması, IAP'ye yapılan değişikliklerdir. reset() vb genellikle foreach'i etkilemez. Örneğin, aşağıdaki kod sanki reset() hiç mevcut değildi:

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

Sebebi ise, reset() geçici olarak IAP'yi değiştirir, döngü gövdesinden sonra geçerli foreach öğesine geri yüklenir. Zorlamak reset() Döngü üzerinde bir etki yapmak için, ek olarak mevcut öğeyi 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. Bunu hatırlarsan gerçek eğlence başlar. HashPointer geri yükleme hala var olup olmadığını belirlemek için öğeye bir işaretçi ve karma kullanır. Ama: Hashes çarpışmalar var ve işaretçiler yeniden kullanılabilir! Bu, dikkatli bir dizi dizi tuşu ile, biz yapabiliriz demektir foreach Kaldırılmış bir öğenin hala var olduğuna inanıyorum, bu yüzden doğrudan ona atlayacaktır. 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 çıkışı beklemeliyiz 1, 1, 3, 4 önceki kurallara göre. Nasıl olur bu 'FYFY' kaldırılan öğe ile aynı karı 'EzFY've ayırıcı, öğeyi depolamak için aynı bellek konumunu yeniden kullanır. Öyleyse foreach, yeni eklenen öğeye doğrudan doğruya atlayarak biter.

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

Bahsetmek istediğim son bir tuhaf durum, PHP'nin döngü sırasında yinelenen varlığı değiştirmenize izin vermesidir. Böylece bir dizide yinelemeye başlayabilir ve daha sonra başka bir diziyle değiştirebilirsin. Veya bir dizide yinelemeye başlayın ve ardından 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örebildiğiniz gibi PHP, ikame gerçekleştiğinde diğer varlığı başlangıçtan itibaren tekrar başlatmaya başlayacaktır.

PHP 7

Hashtable yineleyiciler

Hala hatırlıyorsanız, dizi yineleme ile ilgili ana sorun, yineleme öğelerinin kaldırılması işlemidir. PHP 5, bu amaç için, bir dizi işaretçinin çoklu eşzamanlı foreach döngülerini desteklemek üzere uzatılması gerektiğinden, bir nebze düşük olan tek bir dahili dizi işaretçisi (IAP) kullanmıştır. ve ile etkileşimi reset() Bunun üstüne vb.

PHP 7 farklı bir yaklaşım kullanır, yani isteğe bağlı harici, güvenli karma yineleyiciler oluşturmayı destekler. Bu yineleyiciler diziye kaydedilmeli, hangi noktadan UIM ile aynı semantiklere sahip olmalıdırlar: Bir dizi öğesi kaldırılırsa, o öğeye işaret eden tüm hashtable yineleyiciler bir sonraki öğeye ilerletilir.

Bu, foreach'ın artık IAP'yi kullanmayacağı anlamına gelir hiç. Foreach döngüsü kesinlikle sonuçları üzerinde hiçbir etkisi olmayacaktır current() vb. ve kendi davranışları hiçbir zaman reset() vb.

Dizi çoğaltması

PHP 5 ve PHP 7 arasındaki bir diğer önemli değişiklik, dizi çoğaltması ile ilgilidir. Artık IAP kullanılmadığı için, değer-değer dizisi yinelemesi, tüm durumlarda yalnızca bir refcount artışı (dizinin çoğaltılması yerine) yapar. Dizi foreach döngüsü sırasında değiştirilirse, o noktada bir çoğaltma gerçekleşir (yazılana göre) ve foreach eski dizide çalışmaya devam eder.

Çoğu durumda bu değişiklik şeffaftır ve daha iyi performansa göre başka bir etkisi yoktur. Ancak, 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 */

Referans dizilerinin daha önceki değerine göre yinelemesi özel durumlardı. Bu durumda, çoğaltma gerçekleşmedi, bu nedenle yineleme sırasında dizinin tüm değişiklikleri döngü tarafından yansıtılacaktır. PHP 7'de bu özel durum gider: Bir dizinin bir değerleme iterasyonu her zaman döngü sırasında herhangi bir değişiklik göz ardı ederek, orijinal elemanlar üzerinde çalışmaya devam edin.

Bu, tabi ki, referans olarak yineleme için geçerli değildir. Referansa göre yinelerseniz, tüm değişiklikler döngü tarafından yansıtılacaktır. İlginçtir, aynı düz nesnelerin yinelenen yineleme 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 elleçlenen anlambilimini yansıtır (yani, değere göre bağlamlarda bile referans gibi davranırlar).

Örnekler

Test örnekleriyle başlayarak birkaç örnek düşünelim:

  • Test durumları 1 ve 2 aynı çıkışı korur: Değer-değer dizisi yinelemesi her zaman orijinal elemanlar üzerinde çalışmaya devam eder. (Bu durumda, geri gönderme ve çoğaltma davranışı bile PHP 5 ve PHP 7 arasında aynıdır).

  • Test durumu 3 değişir: Foreach artık IAP'yi kullanmaz. each() döngüden etkilenmez. Aynı çıkış öncesi ve sonrası olacaktır.

  • Test vakaları 4 ve 5 aynı kalır: each() ve reset() foreach hala orijinal diziyi kullanırken IAP'yi değiştirmeden önce diziyi çoğaltacaktır. (Dizi paylaşılmış olsa bile, UİSM değişikliğinin önemi olmaz.)

İkinci örnek örneklerin davranışları ile ilgilidir. current() farklı referans / refcounting konfigürasyonları altında. Bu artık mantıklı değil, current() döngü tarafından tamamen etkilenmez, bu nedenle dönüş değeri her zaman aynı kalır.

Ancak, yineleme sırasında değişiklikler dikkate alındığında bazı ilginç değişiklikler alırız. Umarım yeni davranışları daha aklı başında bulabilirsin. İ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ü ilk iterasyondan sonra artık durmaz. Bunun nedeni, her iki döngünün de artık tamamen ayrı yinelenen yineleyicilere sahip olması ve artık paylaşılan bir IAP aracılığıyla her iki döngünün de çapraz kontaminasyonu olmamasıdır.

Şimdi düzeltilen başka bir garip kenar durumu, aynı karma değere sahip olan öğ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ı yeni öğeye doğru atladı, çünkü "öğütme" öğesiyle aynı "benziyor" gibi görünüyor (çarpışan karma ve işaretçiden dolayı). Artık hiçbir şey için kargaşaya dayanmadığımızdan, bu artık bir sorun değil.


1384
2018-02-13 13:21



@Baba Yapıyor. Bir işleve geçmek, yapmakla aynı şeydir. $foo = $array döngüden önce;) - NikiC
Bir zvalin ne olduğunu bilmeyenler için lütfen Sara Goleman'ın sayfasına bakınız. blog.golemon.com/2007/01/youre-being-lied-to.html - shu zOMG chen
@unbeli PHP tarafından dahili olarak kullanılan terminolojiyi kullanıyorum. Buckets hash çarpışmaları için çift bağlantılı bir listenin parçası ve ayrıca sipariş için iki katına çıkarılmış bir listenin parçasıdır;) - NikiC
@NikiC: Üniversiteye gitmene gerçekten gerek var mı? Sen sadece 19 yaşındasın ama çok daha tecrübeli görünüyorsun - dynamic
Harika anwser. Bence demek istedin iterate($outerArr); ve yok iterate($arr); yere. - niahoo


Örnek 3'te diziyi değiştirmezsiniz. Diğer tüm örneklerde, içeriği veya dahili dizi işaretçisini değiştirirsiniz. Söz konusu olduğunda bu önemlidir PHP Görev operatörü semantiği nedeniyle diziler.

PHP dizileri için atama operatörü daha tembel bir klon gibi çalışır. Bir diziyi içeren bir değişkeni diğerine atamak, çoğu dilden farklı olarak diziyi klonlayacaktır. Ancak, gerçek klonlama, gerekmedikçe yapılmayacaktır. Bu, klonun yalnızca değişkenlerden biri değiştirildiğinde (yazma üzerine yazıldığında) 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önünce, bunu kolayca hayal edebilirsiniz. foreach Diziye referansla bir çeşit yineleyici oluşturur. Bu referans tam olarak değişken gibi çalışır $b benim örneğimde. Bununla birlikte, referans ile birlikte yineleyici sadece döngü sırasında ve sonra ikisi de atılır. Şimdi, tüm durumlarda, ancak 3'ün, bu ekstra referansın canlı olduğu sırada, döngü sırasında dizinin değiştirildiğini görebilirsiniz. Bu bir klonu tetikler ve bu burada neler olduğunu açıklar!

Bu yazma-on-yazma davranışının başka bir yan etkisi için mükemmel bir makale: PHP Üçlü Operatör: Hızlı mı, değil mi?


97
2018-04-07 20:43



Sağ gözüküyor, bunu gösteren bir örnek verdim: codepad.org/OCjtvu8r örneğinizden bir fark - sadece değer değiştirirseniz, değeri değiştirirseniz kopyalanmaz. - zb'
Bu gerçekten yukarıdaki tüm davranış gösterilerini açıklıyor ve güzel bir şekilde çağrılarak gösterilebilir. each() ilk test vakasının sonunda görürüz Dizi, ilk yineleme sırasında değiştirildiğinden, orijinal dizinin dizi işaretçisi ikinci öğeye işaret eder. Bu da şunu gösteriyor ki foreach beklemediğim döngünün kod bloğunu yürütmeden önce dizi işaretçisini hareket ettirir - sonunda bunu yapacağını düşünürdüm. Çok teşekkürler, bu benim için güzelce temizler. - DaveRandom


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

a) foreach üzerinde çalışır beklenen kopya Orijinal dizinin     Foreach () 'a kadar veya bir prospected copy olduğu     oluşturulmamış foreach Notlar / Kullanıcı yorumları.

b) Ne tetikler? beklenen kopya?     Beklenen kopya politikasına dayanarak oluşturulur. copy-on-write, ne zaman, ne zaman     foreach () öğesine geçirilen bir dizi değiştirilir, orijinal dizinin bir kopyası oluşturulur.

c) Orijinal dizi ve foreach () yineleyici DISTINCT SENTINEL VARIABLESYani, orijinal dizi için ve foreach için başka biri; Aşağıdaki test koduna bakınız. SPL , yineleyiciler, ve Dizi Yineleyici.

Yığın taşması sorusu PHP'de '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 örnekte, her () ve reset () öğesinin ETKİLEMEZ. SENTINEL değişkenler (for example, the current index variable) foreach () yineleyicisinin.

$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

34
2018-04-07 21:03



Cevabınız doğru değil. foreach dizinin potansiyel bir kopyası üzerinde çalışır, ancak gerekmedikçe gerçek kopya yapmaz. - linepogl
Bu potansiyel kopyanın kod aracılığıyla nasıl ve ne zaman oluşturulduğunu göstermek ister misiniz? Kodum bunu gösteriyor foreach dizinin zamanının% 100'ünü kopyalıyor. Ben bilmek istekliyim. Yorumlarınız için teşekkürler - sakhunzai
Bir diziyi kopyalamak çok maliyetlidir. Bir dizi kullanarak bir dizi yinelemek için gereken süreyi saymayı deneyin. for veya foreach. İkisi arasında önemli bir fark görmeyeceksiniz, çünkü gerçek bir kopya gerçekleşmez. - linepogl
O zaman var olduğunu varsayardım SHARED data storage kadar veya hariç copy-on-write ancak (kod snippet'imden) her zaman TWO kümesi olacak. SENTINEL variables biri için original array ve diğer için foreach. Teşekkürler bu mantıklı - sakhunzai
"Potansiyel müşteri olarak"? "Korunan" demek istiyor musunuz? - Peter Mortensen


PHP 7 İÇİN NOT

Bazı popülerlik kazanmış olduğundan bu cevabı güncellemek için: Bu cevap artık PHP 7'den itibaren geçerli değildir.Geriye dönük uyumsuz değişiklikler", PHP 7 foreach dizisinin kopyası üzerinde çalışır, böylece dizinin kendisi üzerinde herhangi bir değişiklik foreach döngüsünde yansıtılmamaktadır.

Açıklama (fiyat teklifi php.net):

İlk form array_expression tarafından verilen dizinin üzerine döngüler. Her birinde   yineleme, geçerli elemanın değeri $ değerine ve   iç dizi işaretçisi bir tarafından gelişmiş (böylece sonraki   yineleme, bir sonraki öğeye bakacaksınız).

Yani, ilk örneğinizde dizide sadece bir öğe var ve işaretçi hareket ettirildiğinde bir sonraki öğe mevcut değil, bu yüzden yeni eleman foreach ekledikten sonra son öğe olarak "karar verdi".

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

Bunun her şeyin bir sonucu olduğuna inanıyorum Her yinelemede Belgelerin açıklamalarının bir kısmı, muhtemelen foreach kod çağırmadan önce tüm mantık yapar {}.

Test durumu

Eğer bunu çalıştırırsan:

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

Bu çıktıyı alacaksın:

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

Bu, modifikasyonu kabul ettiği ve "zaman içinde" değiştirildiği için bunu aştığı anlamına gelir. Ama bunu yaparsan:

<?
    $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 foreach zaten dizinin son elemanındaydı, artık “karar vermemeye” karar verdi, ve biz yeni element eklesemize rağmen, “çok geç” ekledik ve ilmeklemedik.

Detaylı açıklama şu adresten okunabilir: PHP 'foreach' aslında nasıl çalışır? Bu davranışın arkasındaki içselleri açıklar.


22
2018-04-15 08:46



Peki cevabın geri kalanını okudun mu? Foreach, başka bir zamana dönüp dönmeyeceğine karar verir. önce Hatta kodu da çalıştırır. - Damir Kasipovic
Hayır, dizi değiştirildi, ancak "çok geç" çünkü foreach, son öğesinde (yinelemenin başlangıcında) olduğunu ve artık dönmeyeceğini "düşünür". İkinci örnekte, yinelemenin başlangıcında son öğede değildir ve bir sonraki yinelemenin başlangıcında yeniden değerlendirilir. Bir test vakası hazırlamaya çalışıyorum. - Damir Kasipovic
@AlmaDo Bakın lxr.php.net/xref/PHP_TRUNK/Zend/zend_vm_def.h#4509 Her zaman yinelendiğinde bir sonraki işaretçiye ayarlanır. Böylece, 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
@ DKasipovic no. Yok tamamla ve temizle orada açıklama (en azından şimdilik - yanılmış olabilirim) - Alma Do
Aslına bakarsanız, @AlmaDo'nun kendi mantığını anlamada bir kusuru vardır. Cevabınız iyidir. - bwoebi


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

Her yinelemede, geçerli elemanın değeri $ v ve dahili olarak atanır.
  dizi işaretçisi bir tarafından geliştirilir (böylece bir sonraki iterasyonda, sonraki öğeye bakarsınız).

İlk örneğinize göre:

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

$array foreach yürütme başına 1 atama, sadece tek eleman var $vve işaretçiyi hareket ettirmek için başka öğeniz yok

Ama ikinci örneğinizde:

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

$array iki eleman var, bu yüzden şimdi $ dizi sıfır endeksleri değerlendirir ve işaretçiyi birer birer hareket ettirir. Döngü ilk iterasyonu için eklendi $array['baz']=3; referans olarak geçmek.


8
2018-04-15 09:32





Büyük soru, çünkü birçok geliştirici, hatta tecrübeli olanlar bile PHP'nin foreach döngülerindeki dizileri işleyişi ile karıştırılıyor. 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şleyişinde şeffaftır. Örneğin:

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

Bu çıktılar:

apple
banana
coconut

Bu nedenle, kopya oluşturulur ancak geliştirici fark etmez, çünkü orijinal dizi döngü içinde veya döngü bittikten sonra başvurulamaz. Ancak, bir döngüdeki öğeleri değiştirmeye çalıştığınızda, bitirdiğinizde bunların değiştirilmediğini 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
)

Orijinaldeki herhangi bir değişiklik farkedilemez. Aslında, bir öğeye açıkça bir değer atamış olsanız bile, orijinalden hiçbir değişiklik yapılmaz. Bunun nedeni, üzerinde çalışılan $ setinin kopyasında göründüğü gibi $ item üzerinde çalışıyor olmanızdır. Bunu, referans olarak $ öğesini 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
)

Bu nedenle, $ item öğesi referans olarak çalıştırıldığında, $ ve item'e yapılan değişiklikler orijinal $ setinin üyelerine yapıldığında belirgin ve gözlemlenebilirdir. Referans ile $ öğesini kullanmak, 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 $ setini kopyaladı ve döngü için kullanıldı, ancak döngü içinde $ set kullanıldığında, PHP kopyalanan dizi yerine orijinal diziye değişkenleri ekledi. Temel olarak, PHP sadece döngü yürütme ve $ öğesinin atama için kopyalanan dizi kullanıyor. Bu nedenle, yukarıdaki döngü yalnızca 3 kez yürütülür ve her seferinde orijinal $ setinin sonuna bir başka değer ekler, orijinal $ setini 6 elemanla bırakır, ancak hiçbir zaman sonsuz bir döngüye girmez.

Ancak, daha önce bahsettiğim gibi, referans olarak $ item kullansaydık? Yukarıdaki sınama tek bir karakter eklendi:

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

Sonsuz döngüde sonuçlar. Bunun aslında sonsuz bir döngü olduğunu unutmayın, komut dosyasını kendiniz öldürmeniz veya işletim sisteminizin bellek yetersiz kalması için beklemeniz gerekir. Scriptime şu satırı ekledim, böylece PHP çok hızlı bir şekilde hafızadan çıkacaktı, bu sonsuz döngü testlerini çalıştırıyorsanız aynı şeyi yapmanızı öneririm:

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

Yani sonsuz döngüdeki bu önceki örnekte, PHP'nin, dizinin döngü için bir kopyasını oluşturmak için yazılmasının nedenini görüyoruz. Bir kopya yalnızca döngü yapısının kendisinin yapısı tarafından oluşturulduğunda ve kullanıldığında, dizi döngü boyunca statik olarak kalır, böylece hiçbir zaman sorunla karşılaşmazsınız.


5
2018-04-21 08:44





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

Foreach döngüsünde, php'nin yaptığı ilk şey, üzerinde yinelenecek olan dizinin bir kopyasını yaratmasıdır. PHP daha sonra bu yeniyi tekrarlar copy orijinal olandan ziyade dizinin. 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 kullanımına izin veriyor iterated values as a reference to the original array value de. 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: İzin vermiyor original array indexes olarak kullanılacak references.

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


4
2017-11-13 14:08



Object public variables yanlış ya da en iyi yanıltıcıdır. Doğru arabirim (örneğin, Traversible) olmadan bir dizide bir nesneyi kullanamazsınız. foreach((array)$obj ... aslında basit bir dizi ile çalışıyorsunuz, artık bir nesne değil. - Christian