Kaip veikia PHP foreach?

Leiskite man prieš tai foreach , sakydamas, kad žinau, kas yra foreach , ir kaip jį naudoti. Šis klausimas yra apie tai, kaip jis veikia po gaubtu, ir man nereikia atsakymų išilgai „tai, kaip jūs susieti masyvą naudodami foreach “.


Ilgą laiką maniau, kad foreach dirbo su pačia masyvu. Tada aš rasiu daug nuorodų į tai, kad jis veikia su masyvo kopija, ir nuo to laiko manau, kad tai yra istorijos pabaiga. Tačiau neseniai pradėjau svarstyti šį klausimą, o po mažo eksperimento paaiškėjo, kad tai nėra 100%.

Leiskite man parodyti, ką turiu galvoje. Šiais bandymų atvejais dirbame su tokia grupe:

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

1 bandymo pavyzdys :

 foreach ($array as $item) { echo "$item\n"; $array[] = $item; } print_r($array);  

Tai aiškiai rodo, kad mes dirbame tiesiogiai su originaliu masyvu - kitaip ciklas tęsis amžinai, nes ciklo metu mes nuolat stumiamės elementus į masyvą. Bet tik įsitikinkite, kad taip yra:

2 bandymo scenarijus :

 foreach ($array as $key => $item) { $array[$key + 1] = $item + 2; echo "$item\n"; } print_r($array);  

Tai patvirtina mūsų pradinę išvadą, mes dirbame su originalo masyvo kopija ciklo metu, kitaip matysime modifikuotas reikšmes ciklo metu. Bet ...

Jei žiūrėsime į vadovą , surasime šį pareiškimą:

Pirmą kartą paleidus foreach, vidinis masyvo rodyklė automatiškai atkuria pirmąjį masyvo elementą.

Tai tiesa ... tai, atrodo, reiškia, kad foreach remiasi šaltinio matricos rodykle. Bet mes tiesiog įrodėme, kad dirbame ne su originaliu masyvu, ar ne? Na, ne iš tikrųjų.

3 bandymo atvejis :

 // 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));  

Taigi, nepaisant to, kad mes dirbame tiesiogiai su šaltinio matrica, mes dirbame tiesiogiai su šaltinio matricos rodykle - tai rodo tai, kad rodyklė yra masyvo pabaigoje ciklo pabaigoje. Be to, tai negali būti teisinga - jei taip būtų, tada 1 bandymo atvejis truks visam laikui.

PHP vadove taip pat nurodyta:

Kadangi foreach remiasi vidinės masyvo žymekliu, jo keitimas kilpa gali sukelti netikėtą elgesį.

Na, išsiaiškinkime, kas yra „netikėtas elgesys“ (techniškai bet koks elgesys yra netikėtas, nes aš nežinau, ko tikėtis).

4 bandymo pavyzdys :

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

5 bandymo scenarijus :

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

... niekas nenuostabu, iš tikrųjų, atrodo, palaiko „šaltinių kopijų“ teoriją.


Klausimas

Kas čia vyksta? Mano „C-fu“ nėra pakankamai gera, kad galėčiau išgauti teisingą išvestį, tik žiūriu į „PHP“ šaltinio kodą.

Man atrodo, kad foreach dirba su masyvo kopija, bet nustato pradinės masyvo masyvo žymeklį iki masyvo pabaigos po kilpos.

  • Ar tai visa istorija?
  • Jei ne, ką ji iš tikrųjų daro?
  • Ar yra situacija, kai naudojantis funkcijomis, kurios sukuria masyvo žymeklį ( each() , reset() ir tt) foreach metu, gali turėti įtakos kilpos rezultatui?
1669
07 апр. nustatė DaveRandom balandžio 7 d 2012-04-07 22:33 '12, 10:33 pm 2012-04-07 22:33
@ 7 atsakymai

foreach palaiko iteraciją per tris skirtingas vertes:

Ateityje bandysiu paaiškinti, kaip iteracija veikia skirtingais atvejais. Žinoma, paprasčiausias atvejis yra objektai, turintys „ Traversable galimybes, nes šiems foreach kodui naudoti tik sintaksinį cukrų:

 foreach ($it as $k => $v) {  }  if ($it instanceof IteratorAggregate) { $it = $it->getIterator(); } for ($it->rewind(); $it->valid(); $it->next()) { $v = $it->current(); $k = $it->key();  } 

Vidinėms klasėms vengiama faktinių metodų, naudojant vidinę API, kuri iš esmės atspindi Iterator sąsają C lygiu.

Iteruojančios matricos ir paprastieji objektai yra daug sudėtingesni. Pirmiausia reikia pažymėti, kad PHP „masyvai“ yra tikrai užsakyti žodynai, ir jie bus perduoti pagal šią tvarką (kuri atitinka įterpimo tvarką, jei nenaudojote kažko panašaus). Tai prieštarauja natūralios raktų eilės kartojimui (kaip dažnai išvardijami darbai kitomis kalbomis) arba neturi konkrečios tvarkos (kaip dažnai žodynai dirba kitomis kalbomis).

Tas pats pasakytina ir apie objektus, nes objekto savybes galima vertinti kaip kitus (užsakytus) žodynų pavadinimus, skirtus žodynui konvertuoti į jų vertybes, taip pat tam tikrą matomumo apdorojimą. Daugeliu atvejų objekto savybės iš tikrųjų nėra saugomos tokiu gana neefektyviu būdu. Tačiau, jei pradėsite pakartotinai per objektą, paprastai naudojama pakuotė bus konvertuojama į tikrą žodyną. Šiuo metu paprastų objektų iteracija yra labai panaši į masyvų iteraciją (tai kodėl aš nekalbu apie paprasto objekto iteraciją).

Viskas vyksta gerai. Žodyno iteracija negali būti pernelyg sudėtinga, ar ne? Problemos prasideda, kai suprantate, kad masyvas / objektas gali keistis iteracijos metu. Tai gali įvykti keliais būdais:

  • Jei kartojate nuorodą naudodami foreach ($arr as > tada $arr virsta nuoroda, ir jūs galite ją pakeisti iteracijos metu.
  • PHP 5, tas pats pasakytina net jei jūs kartojate pagal vertę, bet masyvas buvo iš anksto nuoroda: $ref = $arr; foreach ($ref as $v) $ref = $arr; foreach ($ref as $v) $ref = $arr; foreach ($ref as $v) $ref = $arr; foreach ($ref as $v)
  • Objektai turi metodą, kuris praktiniais tikslais reiškia, kad jie elgiasi kaip nuorodos. Tokiu būdu daiktai visada gali būti keičiami iteracijos metu.

Problema dėl leistinų pakeitimų iteracijos metu yra atvejis, kai šiuo metu esantis elementas ištrinamas. Tarkime, jūs naudojate rodyklę, kad stebėtumėte, kuris masyvo elementas šiuo metu yra. Jei šis elementas yra išleistas, jums bus paliktas kabantis žymeklis (tai paprastai sukelia segfault).

Yra įvairių būdų išspręsti šią problemą. PHP 5 ir PHP 7 šiuo atžvilgiu žymiai skiriasi, ir aš aprašysiu abu elgesį taip. Apibendrinant galima pasakyti, kad PHP 5 metodas buvo gana kvailas ir sukėlė visokių keistų kraštinių problemų, o aukštesnis sąveikos su PHP 7 lygis paskatino nuspėjamesnį ir nuoseklesnį elgesį.

Galiausiai reikia pažymėti, kad PHP naudoja referencinį skaičiavimą ir kopijavimą rašyti atminties valdymui. Tai reiškia, kad jei „kopijuojate“ vertę, iš tikrųjų tiesiog pakartotinai naudosite senąją vertę ir padidinsite jos referencinį skaičių (refcount). Tik atlikus bet kokius pakeitimus bus atlikta tikroji kopija (vadinama „dubliavimu“). Žiūrėkite, ką melavote išsamesniam šios temos pristatymui.

PHP 5

Vidinis masyvo žymeklis ir „HashPointer“

PHP 5 įrenginiuose yra vienas specialus „žymeklis į vidines matricas“ (IAP), kuris teisingai palaiko pakeitimus: kai elementas ištrinamas, bus patikrinta, ar elementas rodo IAP. Tokiu atveju jis pereina prie kito elemento.

Nors foreach naudoja IAP, yra papildoma komplikacija: yra tik vienas IAP, tačiau viena masyvo dalis gali būti kelių foreach ciklų dalis:

 // Using by-ref iteration here to make sure that it really // the same array in both loops and not a copy foreach ($arr as  { foreach ($arr as  { // ... } } 

Norėdami paremti du vienalaikius ciklus su vienu vidiniu masyvo žymekliu, foreach vykdo šiuos scenarijus: prieš vykdant kilpos kūną, foreach grąžina žymeklį į dabartinį elementą ir jo HashPointer HashPointer -foreach. Kai pradės veikti kilpos korpusas, IAP bus grąžintas į šį elementą, jei jis vis dar egzistuoja. Tačiau, jei elementas buvo ištrintas, naudosime tik tai, kas šiuo metu yra IAP. Ši schema iš esmės yra natūra, tačiau yra daug keistų elgesių, dėl kurių galite išeiti, o kai kurie iš jų bus parodyti toliau.

Dublikatas

IAP yra matoma masyvo funkcija (rodoma per current funkcijų šeimą), nes tokie IAP pakeitimai yra laikomi kopijavimo-rašymo semantikos modifikacijomis. Tai, deja, reiškia, kad daugeliu atvejų foreach verčia kartoti masyvą, kurį ji kartoja. Tikslios sąlygos:

  1. Masyvas nėra nuoroda (is_ref = 0). Jei tai yra nuoroda, tada jos pakeitimai turėtų būti paskirstyti, todėl jis neturėtų būti dubliuojamas.
  2. Masyvas turi refcount> 1. Jei atsiskaitymas yra 1, masyvas nenaudojamas, ir mes galime ją laisvai keisti.

Jei masyvas yra ne dubliuotas (is_ref = 0, refcount = 1), tada tik jo grąža padidės (*). Be to, jei foreach yra naudojamas kaip nuoroda, tuomet (potencialiai pasikartojanti) masyvas bus paverstas nuoroda.

Apsvarstykite šį kodą kaip pavyzdį, kai vyksta dubliavimas:

 function iterate($arr) { foreach ($arr as $v) {} } $outerArr = [0, 1, 2, 3, 4]; iterate($outerArr); 

Čia $arr bus dubliuota, kad būtų išvengta IAP pakeitimų nuo $arr nuo nutekėjimo iki $outerArr . Atsižvelgiant į pirmiau minėtas sąlygas, masyvas nėra nuoroda (is_ref = 0) ir naudojamas dviejose vietose (refcount = 2). Šis reikalavimas yra nesėkmingas ir yra suboptimalaus įgyvendinimo artefaktas (iteracijos metu nėra jokių pakeitimų, todėl iš tikrųjų nereikia naudoti IAP).

(*) Padidėjęs prieaugis čia skamba nekenksmingai, tačiau pažeidžia kopijavimo-rašymo (COW) semantiką: tai reiškia, kad mes pakeisime refcount = 2 masyvo IAP, o COW nurodo, kad pakeitimus galima atlikti tik perskaičiavus = 1. Šis pažeidimas lemia naudotojo elgesio pasikeitimą (o COW paprastai yra skaidrus), nes IAP pasikeitimas iteruotoje matricoje bus pastebimas - bet tik iki pirmojo pakeitimo, kuris skiriasi nuo masyvo IAP. Vietoj to, trys „tikrieji“ parametrai būtų: a) visada dubliuoti, b) nepadidinti konversijos koeficiento ir tokiu būdu leisti atsitiktinai modifikuoti iteruotą masę kilpoje arba c) nenaudoti IAP (PHP 7 sprendimas).

Pozicijos skatinimo procedūra

Yra viena galutinė įgyvendinimo informacija, kurią reikia žinoti, kad būtų galima tinkamai suprasti toliau pateiktus kodų pavyzdžius. Pseudokode „įprastas“ būdas perjungti kai kurių duomenų struktūrą atrodys panašus:

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

Vis dėlto, kad jis yra gana ypatingas sniego suktukas, jis norėtų kažką daryti kitaip:

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

Būtent, masyvo rodyklė jau juda į priekį prieš pradedant kilpos korpusą. Tai reiškia, kad nors kilpos korpusas dirba su elementu $i , IAP jau yra elemente $i+1 . Todėl kodavimo pavyzdžiai, rodantys modifikaciją iteracijos metu, visada atšaukia kitą elementą, o ne esamą.

Pavyzdžiai: Jūsų bandymo pavyzdžiai

Trys pirmiau aprašyti aspektai iš esmės turėtų suteikti jums pilną vaizdą apie foreach įgyvendinimo idiokratiją, ir mes galime aptarti keletą pavyzdžių.

Jūsų bandymų atvejų elgesį šiuo metu lengva paaiškinti:

  • 1 ir 2 bandymų atvejais $array prasideda skaičiuojant = 1, todėl jis nebus dubliuojamas foreach: tik padidėja skaičiavimas. Kai kilpos kėbulas vėliau keičia masyvą (kuris šiuo metu turi refcount = 2), šiuo metu bus dubliavimasis. „Foreach“ toliau dirbs nepakeistą $array kopiją.

  • 3 bandymo pavyzdyje masyvas dar nepasikartojamas, todėl foreach pakeis IAP kintamąjį $array . Pasibaigus iteracijai, MAP yra NULL (t. Y. Daroma iteracija), kurių each rodo grįžtamąjį false .

  • 4 ir 5 bandymų pavyzdžiai, each reset jų ir reset nustatyti pagal atskaitos funkcijas. $array , kai jis perduodamas jiems, turi refcount=2 , todėl jis turi būti dubliuotas. Kadangi šis foreach vėl veiks su atskiru masyvu.

Pavyzdžiai: current įtaka foreach

Geras būdas parodyti įvairių dublikatų elgesį yra stebėti current() funkcijos elgseną foreach kilpoje. Apsvarstykite šį pavyzdį:

 foreach ($array as $val) { var_dump(current($array)); }  

Čia turėtumėte žinoti, kad current() yra šalutinė funkcija (iš tikrųjų: pageidaujama-ref), nors ji nepakeičia masyvo. Tai turi būti, kad būtų gerai žaidžiamas su visomis kitomis funkcijomis, pvz. Perėjimo baitai reiškia, kad masyvas turi būti suskirstytas, todėl $array ir $array foreach bus skirtingi. Priežastis, kodėl gausite 2 vietoj 1 taip pat paminėta aukščiau: foreach skatina masyvo žymeklį prieš paleisdamas vartotojo kodą, o ne po. Todėl, nors kodas yra pirmame elemente, foreach jau perkėlė rodyklę į antrąjį.

Dabar išbandykime nedidelį pakeitimą:

 $ref =  foreach ($array as $val) { var_dump(current($array)); }  

Čia mes turime atvejį is_ref = 1, todėl masyvas nėra nukopijuotas (kaip nurodyta aukščiau). Bet dabar, kai tai yra nuoroda, masyvas neturėtų būti dubliuojamas, kai pereinama prie by-ref current() funkcijos. Taigi, current() ir foreach dirba su ta pačia matrica. Tačiau vis dar matote elgesį atskirai, nes foreach skatina rodyklę.

Pakartodami iteraciją, jūs gaunate tą patį elgesį:

 foreach ($array as  { var_dump(current($array)); }  

Svarbi dalis čia yra ta, kad foreach atliks $array a is_ref = 1, kai ji kartojama pagal nuorodą, todėl iš esmės turite tokią pačią situaciją kaip ir anksčiau.

Kitas mažas variantas, šį kartą mes priskiriame masyvą kitam kintamajam:

 $foo = $array; foreach ($array as $val) { var_dump(current($array)); }  

Čia, skaičiuojant $array yra 2, kai ciklas veikia, todėl šį kartą mes turime iš anksto atlikti dubliavimą. Tokiu būdu, $array ir masyvas, naudojamas foreach, bus visiškai atskirti nuo pradžios. Štai kodėl jūs gaunate IAP poziciją visur, kur jis buvo prieš ciklą (šiuo atveju jis buvo pirmoje pozicijoje).

Pavyzdžiai: keitimas iteracijos metu

Bandymas atsižvelgti į pokyčius iteracijos metu yra tas, kur atsirado visos problemos, susijusios su foreach'u, todėl ji padeda apsvarstyti keletą pavyzdžių.

Apsvarstykite šias įdėtas kilpas per tą patį masyvą (kur naudojama pakartotinė iteracija, siekiant įsitikinti, kad ji yra tokia pati):

 foreach ($array as  { foreach ($array as  { if ($v1 == 1  $v2 == 1) { unset($array[1]); } echo "($v1, $v2)\n"; } } // Output: (1, 1) (1, 3) (1, 4) (1, 5) 

Tikimasi, kad čia (1, 2) trūksta išėjimo, nes elementas 1 pašalinamas. Tikriausiai netikėta, kad išorinė kilpa sustoja po pirmojo elemento. Kodėl taip?

To priežastis yra pirmiau aprašyti įdėtos kilpos įsilaužėliai: prieš kilpos HashPointer yra „ HashPointer , dabartinė IAP padėtis ir maišos bus nukopijuotos į „ HashPointer . Po kilpos kūno, jis bus atkurtas, bet tik tuo atveju, jei elementas vis dar egzistuoja, kitaip vietoj dabartinės IAP pozicijos (nepriklausomai nuo to). Pirmiau pateiktame pavyzdyje būtent taip yra: esamas išorinio kontūro elementas buvo ištrintas, todėl bus naudojamas IAP, kuris jau buvo pažymėtas kaip užbaigtas vidinės kilpos!

Kita „ HashPointer atsarginės kopijos + atstatymo „ HashPointer yra ta, kad IAP pakeitimai, nors ir reset() , ir tt Paprastai jie neturi įtakos foreach. Pvz., Šis kodas veikia taip, tarsi jei reset() visai nebūtų buvę:

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

Priežastis yra ta, kad nors reset() laikinai pakeičia IAP, jis bus grąžintas į dabartinį foreach elementą po kilpos korpuso. Jei norite, kad reset() paveiktų kilpą, turite papildomai ištrinti dabartinį elementą, kad atsarginės kopijos / atkūrimo mechanizmas baigtųsi klaida:

 $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 

Tačiau šie pavyzdžiai vis dar yra normalūs. Tikrasis linksmumas prasideda, jei prisimenate, kad „ HashPointer naudoja rodyklę prie elemento ir jo maišos, kad nustatytų, ar jis vis dar egzistuoja. Tačiau: maišos turi susidūrimų, o rodyklės gali būti pakartotinai naudojamos! Tai reiškia, kad kruopščiai atrenkant masyvo raktus galime pasitikėti, kad ištrintas elementas vis dar egzistuoja, todėl jis bus tiesiogiai peršokęs į jį. Pavyzdys:

 $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 

Čia paprastai turime laukti 1, 1, 3, 4 išėjimo pagal ankstesnes taisykles. Kas atsitinka, 'FYFY' turi tą patį maišą, kaip ir nuotolinis 'EzFY' elementas, o 'EzFY' naudoja tą pačią atminties vietą elementui išsaugoti. Taigi foreach eina tiesiai į naujai įterptą elementą, taip sumažindamas kilpą.

Pakartotinio objekto pakeitimas ciklo metu

Paskutinis atvejis, kurį norėčiau paminėti, yra tai, kad PHP leidžia jums pakeisti iteruotą objektą kilpos metu. Taigi, galite pradėti iteraciją vienoje matricoje, o po to ją pakeisti kita masyvo pusė. Arba pradėkite pakartoti masyvą ir tada jį pakeisti objektu:

 $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; } }  

Kaip matote, šiuo atveju PHP pradės kartoti kito objekto iteraciją nuo pat pradžių, kai tik atsiranda pakeitimo.

PHP 7

Hashtable Iterators

Jei vis dar prisimenate, pagrindinė problema, susijusi su masyvo iteracija, buvo tai, kaip apdoroti elementų ištrinimą iteracijos viduryje. PHP 5 šiam tikslui naudojo vieną vidinį masyvo žymeklį (IAP), kuris buvo šiek tiek suboptimalus, nes vienas rodyklė prie masyvo turėjo būti ištempta, kad palaikytų kelis vienalaikius foreach ciklus ir sąveiką su reset() ir kt. Кроме этого.