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 Traversable
nesnelerdir, çünkü bunlar foreach
iç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 Iterator
C 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 $arr
bir 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 foreach
tek IAP yoktur, ancak bir dizi çoklu parçası olabilir: İEN'nin yapmak kullanımını yapar ülkede ek bir komplikasyonudur foreach
dö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, foreach
bir 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 ( current
iş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:
- 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.
- Dizide refcount> 1 var. Eğer
refcount
1, 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 refcount
artırılır (*). Ek olarak, foreach
referans 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, $arr
IAP değişikliklerinin $arr
sı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).
(*) refcount
Burada 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) refcount
artmaz 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 foreach
uygulamanı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 refcount
artı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 foreach
IAP'sini değiştirir $array
. Yinelemenin sonunda, IAP NULL'dur (yani yineleme yapıldı), bu da each
geri dönerek gösterir false
.
Test durumları 4 ve 5'in her ikisi de each
ve reset
referans fonksiyonlarıdır. $array
Bir sahiptir refcount=2
o çoğaltılamaz zorundadır yüzden onlara geçirildiğinde. Bu foreach
yüzden yine ayrı bir dizi üzerinde çalışacaktır.
Örnekler: current
in foreach'in etkileri
Çeşitli çoğaltma davranışlarını göstermenin iyi bir yolu, current()
işlevin bir foreach
dö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 next
ref 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 $array
ve foreach-array
farklı olacaktır. Bunun 2
yerine almanızın nedeni 1
yukarı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 foreach
aynı 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, $array
foreach'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 $array
dö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 1
kaldı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!
HashPointer
Yedekleme + 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, HashPointer
geri 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, foreach
kaldı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 foreach
artık IAP kullanacak hiç . foreach
Dö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 refcount
artış (diziyi çoğaltmak yerine) yapacaktır . Dizi foreach
döngü sırasında değiştirilirse , bu noktada bir çoğaltma gerçekleşir (yazma üzerine kopyalamaya göre) ve foreach
eski 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 refcounting
ve yineleme davranışı PHP 5 ve PHP 7 arasında tam olarak aynıdır).
Test durumu 3 değişiklikleri: Foreach
artı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, foreach
yine 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/refcounting
konfigü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.