Vraag Hoe werkt PHP 'foreach' eigenlijk?


Laat me dit voorvoegen door te zeggen dat ik weet wat foreach is, doet en hoe het te gebruiken. Deze vraag gaat over hoe het werkt onder de motorkap, en ik wil geen antwoorden in de trant van "dit is hoe je een array met foreach".


Lange tijd heb ik dat aangenomen foreach werkte met de array zelf. Toen vond ik veel verwijzingen naar het feit dat het werkt met een kopiëren van het array, en ik heb sindsdien aangenomen dat dit het einde van het verhaal is. Maar ik kwam onlangs in een discussie over de zaak en na een beetje experimentatie bleek dat dit niet 100% waar was.

Laat me laten zien wat ik bedoel. Voor de volgende testgevallen zullen we werken met de volgende array:

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

Test case 1:

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

/* Output in loop:    1 2 3 4 5
   $array after loop: 1 2 3 4 5 1 2 3 4 5 */

Dit laat duidelijk zien dat we niet direct met de source array werken - anders zou de lus voor altijd doorgaan, omdat we constant items op de array pushen tijdens de lus. Maar om zeker te zijn is dit het geval:

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

Dit ondersteunt onze eerste conclusie, we werken met een kopie van de bronarray tijdens de lus, anders zouden we de gewijzigde waarden tijdens de lus zien. Maar...

Als we kijken in de met de hand, we vinden deze verklaring:

Wanneer voor het eerst de uitvoering begint, wordt de interne matrixaanwijzer automatisch opnieuw ingesteld op het eerste element van de array.

Juist ... dit lijkt dat te suggereren foreach vertrouwt op de array-aanwijzer van de bronarray. Maar we hebben net bewezen dat we dat zijn werkt niet met de bronarray, toch? Nou, niet helemaal.

Test case 3:

// Move the array pointer on one to make sure it doesn't affect the loop
var_dump(each($array));

foreach ($array as $item) {
  echo "$item\n";
}

var_dump(each($array));

/* Output
  array(4) {
    [1]=>
    int(1)
    ["value"]=>
    int(1)
    [0]=>
    int(0)
    ["key"]=>
    int(0)
  }
  1
  2
  3
  4
  5
  bool(false)
*/

Dus, ondanks het feit dat we niet direct met de bronarray werken, werken we direct met de source array-aanwijzer - het feit dat de aanwijzer zich aan het einde van de array aan het einde van de lus bevindt, toont dit. Maar dit kan niet waar zijn - als dat zo was, dan testcase 1 zou lus voor altijd.

In de PHP-handleiding staat ook:

Zoals voor iedereen vertrouwt op de interne array-pointer die het binnen de lus verandert, kan dit leiden tot onverwacht gedrag.

Laten we eens kijken wat dat "onverwachte gedrag" is (technisch gezien is elk gedrag onverwacht, omdat ik niet meer weet wat ik kan verwachten).

Testcase 4:

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

/* Output: 1 2 3 4 5 */

Test case 5:

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

/* Output: 1 2 3 4 5 */

... niets dat daar onverwachts is, lijkt in feite de theorie van "kopie van de bron" te ondersteunen.


De vraag

Wat is hier aan de hand? Mijn C-fu is niet goed genoeg voor mij om een ​​goede conclusie te trekken door simpelweg naar de PHP-broncode te kijken, ik zou het op prijs stellen als iemand het voor mij in het Engels zou kunnen vertalen.

Het lijkt me dat foreach werkt met een kopiëren van de array, maar stelt de array-aanwijzer van de bronarray na de lus in op het einde van de array.

  • Klopt dit en het hele verhaal?
  • Zo nee, wat doet het echt?
  • Is er een situatie waarbij functies worden gebruikt die de array-aanwijzer aanpassen (each(), reset() et al.) tijdens een foreach kan de uitkomst van de lus beïnvloeden?

1637
2018-04-07 19:33


oorsprong


antwoorden:


foreach ondersteunt iteratie over drie verschillende soorten waarden:

In het volgende zal ik proberen precies uit te leggen hoe iteratie in de verschillende gevallen werkt. Verreweg de eenvoudigste zijn Traversable objecten, zoals voor deze foreach is in wezen alleen syntaxisuiker voor code volgens deze regels:

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

Voor interne klassen worden werkelijke methode-aanroepen vermeden door een interne API te gebruiken die in essentie alleen de Iterator interface op het C-niveau.

Iteratie van arrays en gewone objecten is aanzienlijk gecompliceerder. Allereerst moet worden opgemerkt dat in PHP "arrays" echt geordende woordenboeken zijn en dat ze worden doorlopen volgens deze volgorde (die overeenkomt met de invoegopdracht, zolang u niet zoiets hebt gebruikt als sort). Dit is in tegenstelling tot itereren door de natuurlijke volgorde van de toetsen (hoe lijsten in andere talen vaak werken) of helemaal geen gedefinieerde volgorde hebben (hoe woordenboeken in andere talen vaak werken).

Hetzelfde geldt ook voor objecten, omdat de objecteigenschappen kunnen worden gezien als een andere (geordende) naam van de dictionary-toewijzing van onroerend goed voor hun waarden, plus enige afhandeling van de zichtbaarheid. In de meeste gevallen worden de objecteigenschappen niet echt op deze nogal inefficiënte manier opgeslagen. Als u echter begint met itereren over een object, wordt de gepakte representatie die normaal wordt gebruikt, omgezet in een echt woordenboek. Op dat moment komt iteratie van gewone objecten sterk overeen met iteratie van arrays (daarom bespreek ik hier niet veel over object-iteratie).

Tot nu toe, zo goed. Itereren over een woordenboek kan niet al te moeilijk zijn, toch? De problemen beginnen wanneer u zich realiseert dat een array / object tijdens iteratie kan veranderen. Er zijn meerdere manieren waarop dit kan gebeuren:

  • Als u itereert door te verwijzen met foreach ($arr as &$v) dan $arr wordt omgezet in een referentie en je kunt het tijdens de iteratie wijzigen.
  • In PHP 5 is hetzelfde van toepassing, zelfs als je itereert naar waarde, maar de array was vooraf een referentie: $ref =& $arr; foreach ($ref as $v)
  • Objecten hebben een insteek voorbijgaande semantiek, wat voor praktische doeleinden betekent dat ze zich gedragen als referenties. Dus objecten kunnen altijd worden gewijzigd tijdens iteratie.

Het probleem met het toestaan ​​van wijzigingen tijdens iteratie is het geval waarbij het element waar u zich op dat moment bevindt, is verwijderd. Stel dat u een aanwijzer gebruikt om bij te houden welk arrayelement u op dat moment bent. Als dit element nu is vrijgegeven, blijft er een hangende aanwijzer over (meestal resulteert dit in een segfault).

Er zijn verschillende manieren om dit probleem op te lossen. PHP 5 en PHP 7 verschillen in dit opzicht aanzienlijk en ik zal beide gedragingen in het volgende beschrijven. De samenvatting is dat de aanpak van PHP 5 tamelijk dom was en leidde tot allerlei rare edge-case-problemen, terwijl de meer betrokken benadering van PHP 7 resulteert in meer voorspelbaar en consistent gedrag.

Als laatste inleidende opmerking moet worden opgemerkt dat PHP gebruikmaakt van referentietelling en copy-on-write om het geheugen te beheren. Dit betekent dat als u een waarde "kopieert", u gewoon de oude waarde opnieuw gebruikt en het referentietellingsteam verhoogt (opnieuw rekenen). Pas als u een of andere wijziging uitvoert, wordt een echte kopie (een "duplicatie") uitgevoerd. Zien Je wordt voorgelogen voor een uitgebreidere introductie over dit onderwerp.

PHP 5

Interne array-aanwijzer en HashPointer

Arrays in PHP 5 hebben één speciale "interne array pointer" (IAP), die modificaties goed ondersteunt: Wanneer een element wordt verwijderd, zal er een controle zijn of de IAP naar dit element verwijst. Als dat zo is, wordt in plaats daarvan doorgeschoven naar het volgende element.

Hoewel foreach wel gebruik maakt van de IAP, is er een extra complicatie: er is slechts één IAP, maar één array kan deel uitmaken van meerdere foreach-loops:

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

Ter ondersteuning van twee gelijktijdige lussen met slechts één interne arrayaanwijzer voert foreach de volgende schannen uit: Voordat de lus wordt uitgevoerd, maakt foreach een verwijzing naar het huidige element en de hash ervan in een per-foreach HashPointer. Nadat het luslichaam is uitgevoerd, wordt de IAP teruggezet naar dit element als het nog steeds bestaat. Als het element echter is verwijderd, gebruiken we alleen waar het IAP zich momenteel bevindt. Dit schema is meestal een beetje-soort van werken, maar er is veel vreemd gedrag dat je eruit kunt halen, waarvan ik hieronder enkele voorbeelden zal geven.

Array-duplicatie

De IAP is een zichtbaar kenmerk van een array (weergegeven via de current functiefamilie), aangezien dergelijke wijzigingen in de IAP worden beschouwd als wijzigingen onder de copy-on-write-semantiek. Dit betekent helaas dat foreach in veel gevallen wordt gedwongen om de array te dupliceren waar het overheen loopt. De precieze voorwaarden zijn:

  1. De array is geen verwijzing (is_ref = 0). Als het een referentie is, dan zijn wijzigingen erin vermeend te verspreiden, dus het moet niet worden gedupliceerd.
  2. De array heeft een waarde van> 1. Als refcount 1 is, wordt de array niet gedeeld en kunnen we deze rechtstreeks wijzigen.

Als de array niet wordt gedupliceerd (is_ref = 0, refcount = 1), wordt alleen de nieuwe teller verhoogd (*). Als bovendien voor elke referentie wordt gebruikt, wordt de (mogelijk gedupliceerde) array omgezet in een verwijzing.

Beschouw deze code als een voorbeeld waarbij duplicatie optreedt:

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

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

Hier, $arr wordt gekopieerd om te voorkomen dat IAP-wijzigingen worden doorgevoerd $arr van lekken naar $outerArr. In termen van de bovenstaande voorwaarden is de array geen referentie (is_ref = 0) en wordt deze op twee plaatsen gebruikt (refcount = 2). Deze vereiste is ongelukkig en een artefact van de suboptimale implementatie (er is geen reden tot modificatie tijdens iteratie hier, dus we hoeven het IAP in de eerste plaats niet te gebruiken).

(*) Het verhogen van de refcount klinkt hier onschuldig, maar schendt de copy-on-write (COW) semantiek: dit betekent dat we de IAP van een refcount = 2 array gaan wijzigen, terwijl COW dicteert dat wijzigingen alleen kunnen worden uitgevoerd op refcount = 1 waarden. Deze overtreding resulteert in gedragsverandering door gebruikers (terwijl COW normaal transparant is), omdat de IAP-wijziging op de geïtereerde array waarneembaar is - maar alleen tot de eerste niet-IAP-wijziging op de array. In plaats daarvan zouden de drie "geldige" opties zijn geweest a) om altijd te dupliceren, b) om de refcount niet te verhogen en zo de iteratie van de array willekeurig in de lus te wijzigen, of c) helemaal geen IAP te gebruiken ( de PHP 7-oplossing).

Volgorde volgorde plaatsen

Er is een laatste implementatiedetail waar u op moet letten om onderstaande codevoorbeelden goed te kunnen begrijpen. De "normale" manier van het doorlopen van een bepaalde datastructuur zou er ongeveer zo uitzien in pseudocode:

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

Echter foreach, zijnde een nogal speciale sneeuwvlok, kiest ervoor om dingen een beetje anders te doen:

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

De array-aanwijzer is namelijk al naar voren verplaatst voor het luslichaam werkt. Dit betekent dat terwijl het luslichaam aan element werkt $i, het IAP is al elementair $i+1. Dit is de reden waarom codevoorbeelden die modificatie tonen tijdens iteratie altijd de optie uitschakelen volgende element, in plaats van de huidige.

Voorbeelden: uw testcases

De drie hierboven beschreven aspecten zouden u een grotendeels volledige indruk moeten geven van de eigenaardigheden van de voor elke implementatie en we kunnen verder gaan met het bespreken van enkele voorbeelden.

Het gedrag van uw testcases is op dit moment eenvoudig uit te leggen:

  • In testgevallen 1 en 2 $array begint met refcount = 1, dus wordt het niet voor $ elke keer gedupliceerd: alleen de hervatting wordt opgehoogd. Wanneer het luslichaam vervolgens de array wijzigt (die refcount = 2 op dat punt heeft), zal de duplicatie op dat punt plaatsvinden. Foreach zal blijven werken aan een ongewijzigde kopie van $array.

  • In testcase 3 wordt de array opnieuw niet gedupliceerd, dus foreach zal de IAP van de $array variabel. Aan het einde van de iteratie is de IAP NULL (wat betekent dat de iteratie is voltooid), wat each geeft aan door terug te keren false.

  • In testgevallen 4 en 5 allebei each en reset zijn naslagfuncties. De $array heeft een refcount=2 wanneer het aan hen wordt doorgegeven, moet het dus worden gedupliceerd. Als zodanig foreach zal weer aan een aparte array werken.

Voorbeelden: effecten van current in voor iedereen

Een goede manier om de verschillende duplicatiegedragingen te tonen, is het gedrag van de current() functie binnen een foreach-lus. Beschouw dit voorbeeld:

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

Hier zou je dat moeten weten current() is een by-ref-functie (eigenlijk: prefer-ref), ook als deze de array niet wijzigt. Het moet zo zijn om leuk te spelen met alle andere functies, zoals next die allemaal door-ref zijn. Doorverwijzing geeft aan dat de array moet worden gescheiden en dus $array en de foreach-array zal anders zijn. De reden waarom je krijgt 2 in plaats van 1 wordt ook hierboven vermeld: foreach verplaatst de array-aanwijzer voor de gebruikerscode uitvoeren, niet daarna. Dus hoewel de code het eerste element is, is de aanwijzer al vooruitgeschoven naar de tweede.

Laten we nu een kleine wijziging proberen:

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

Hier hebben we het geval is_ref = 1, dus de array wordt niet gekopieerd (net als hierboven). Maar nu het een referentie is, hoeft de array niet langer te worden gedupliceerd wanneer deze wordt doorgegeven aan de by-ref current() functie. Dus current() en voor elk werk op dezelfde array. Je ziet echter nog steeds het off-by-one gedrag als gevolg van de manier waarop foreachverplaatst de aanwijzer.

Je krijgt hetzelfde gedrag bij het uitvoeren van een by-ref iteratie:

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

Hier is het belangrijke deel dat voor ieder zal maken $array een is_ref = 1 wanneer het wordt geïtereerd door verwijzing, dus eigenlijk heb je dezelfde situatie als hierboven.

Nog een kleine variatie, deze keer zullen we de array toewijzen aan een andere variabele:

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

Hier de refcount van de $array is 2 wanneer de lus wordt gestart, dus voor een keer moeten we de duplicatie van tevoren doen. Dus $array en de array gebruikt door foreach zal vanaf het begin volledig gescheiden zijn. Daarom krijg je de positie van het IAP waar het ook was vóór de lus (in dit geval was het op de eerste positie).

Voorbeelden: modificatie tijdens iteratie

Proberen om rekening te houden met wijzigingen tijdens iteratie is waar al onze foreach problemen vandaan kwamen, dus het dient om enkele voorbeelden voor deze zaak te overwegen.

Beschouw deze geneste lussen over dezelfde array (waarbij by-ref iteratie wordt gebruikt om ervoor te zorgen dat het dezelfde is):

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)

Het verwachte deel hier is dat (1, 2) ontbreekt in de uitvoer, omdat element 1 was verwijderd. Wat waarschijnlijk onverwacht is, is dat de buitenste lus stopt na het eerste element. Waarom is dat?

De reden hiervoor is de hierboven beschreven geneste lus-hack: voordat de lus wordt uitgevoerd, wordt de huidige IAP-positie en hash opgeslagen in een back-up HashPointer. Na het luslichaam wordt het hersteld, maar alleen als het element nog steeds bestaat, anders wordt de huidige IAP-positie (wat deze ook is) gebruikt. In het bovenstaande voorbeeld is dit precies het geval: het huidige element van de buitenste lus is verwijderd, dus het gebruikt de IAP, die al is gemarkeerd als voltooid door de binnenste lus!

Een ander gevolg van de HashPointer back-up + herstelmechanisme is dat echter wijzigingen in de IAP reset() enz. hebben meestal geen invloed op iedereen. De volgende code wordt bijvoorbeeld uitgevoerd alsof het reset() waren helemaal niet aanwezig:

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

De reden is dat, terwijl reset() wijzigt tijdelijk de IAP, deze wordt hersteld naar het huidige foreach-element na de body van de lus. Dwingen reset() om een ​​effect op de lus te maken, moet u bovendien het huidige element verwijderen, zodat het back-up / herstelmechanisme mislukt:

$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

Maar die voorbeelden zijn nog steeds gezond. Het echte plezier begint als je je herinnert dat het HashPointer herstel gebruikt een aanwijzer naar het element en zijn hash om te bepalen of het nog steeds bestaat. Maar: hash hebben botsingen en wijzers kunnen worden hergebruikt! Dit betekent dat we met een zorgvuldige selectie van array-sleutels kunnen maken foreach geloven dat een element dat is verwijderd nog steeds bestaat, dus het zal er direct naar toe springen. Een voorbeeld:

$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

Hier zouden we normaal de output moeten verwachten 1, 1, 3, 4 volgens de vorige regels. Hoe wat gebeurt, is dat 'FYFY' heeft dezelfde hash als het verwijderde element 'EzFY'en de allocator gebeurt om dezelfde geheugenlocatie opnieuw te gebruiken om het element op te slaan. Dus foreach eindigt direct met springen naar het nieuw ingebrachte element, waardoor de lus kortgesloten wordt.

Vervanging van de geïtereerde entiteit tijdens de lus

Een laatste vreemde zaak die ik zou willen noemen, is dat PHP je toestaat om de geïtereerde entiteit te vervangen tijdens de lus. U kunt dus beginnen met itereren op één array en deze halverwege vervangen door een andere array. Of begin met het herhalen van een array en vervang deze door een object:

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

Zoals je in dit geval kunt zien, zal PHP vanaf het begin beginnen met het herhalen van de andere entiteit zodra de vervanging is gebeurd.

PHP 7

Hashtable iterators

Als je het je nog herinnert, was het belangrijkste probleem met array-iteratie hoe om te gaan met het verwijderen van elementen in iteratie. PHP 5 gebruikte hiervoor een enkele interne array-aanwijzer (IAP), die enigszins suboptimaal was, omdat één array-aanwijzer moest worden uitgerekt om meerdere gelijktijdige foreach-loops te ondersteunen en interactie met reset() etc. daarbovenop.

PHP 7 gebruikt een andere benadering, namelijk het ondersteunt het creëren van een willekeurige hoeveelheid externe, veilige hashtable iterators. Deze iterators moeten in de array worden geregistreerd, vanaf welk punt ze dezelfde semantiek hebben als de IAP: als een arrayelement wordt verwijderd, worden alle hashtable iterators die naar dat element wijzen naar het volgende element doorgeschoven.

Dit betekent dat foreach het IAP niet langer zal gebruiken helemaal niet. De foreach-lus heeft absoluut geen effect op de resultaten van current() enz. en zijn eigen gedrag zal nooit worden beïnvloed door functies zoals reset() enz.

Array-duplicatie

Een andere belangrijke verandering tussen PHP 5 en PHP 7 heeft betrekking op array-duplicatie. Nu dat de IAP niet langer wordt gebruikt, zal attribuutarray-iteratie in alle gevallen alleen een herhalingsincrement (in plaats van duplicatie van de array) doen. Als de array wordt gewijzigd tijdens de foreach-lus, gebeurt er op dat moment een duplicatie (volgens copy-on-write) en blijft foreach werken aan de oude array.

In de meeste gevallen is deze wijziging transparant en heeft deze geen ander effect dan betere prestaties. Er is echter één gelegenheid waar het tot ander gedrag leidt, namelijk het geval waarbij de array van te voren een referentie was:

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

Vroegere by-value iteratie van referentie-arrays was een speciaal geval. In dit geval trad geen duplicatie op, dus alle wijzigingen van de array tijdens iteratie zouden door de lus worden gereflecteerd. In PHP 7 is dit speciale geval weg: een by-value iteratie van een array zal altijd blijf werken aan de originele elementen, zonder rekening te houden met eventuele wijzigingen tijdens de lus.

Dit is natuurlijk niet van toepassing op verwijzingen naar verwijzingen. Als u doorverwijzing herhaalt, worden alle wijzigingen door de lus weerspiegeld. Interessant is dat hetzelfde geldt voor bytewaarde iteratie van gewone objecten:

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

Dit weerspiegelt by-handle semantiek van objecten (d.w.z. ze gedragen zich als een referentie, zelfs in by-value contexten).

Voorbeelden

Laten we een paar voorbeelden bekijken, te beginnen met uw testcases:

  • Test cases 1 en 2 behouden dezelfde output: By-value array-iteratie werkt altijd aan de originele elementen. (In dit geval is zelfs herpattings- en duplicatiegedrag precies hetzelfde tussen PHP 5 en PHP 7).

  • Testcase 3 verandert: Foreach maakt niet langer gebruik van het IAP, dus each() wordt niet beïnvloed door de lus. Het zal dezelfde uitvoer voor en na hebben.

  • Testgevallen 4 en 5 blijven hetzelfde: each() en reset() dupliceert de array voordat de IAP wordt gewijzigd, terwijl foreach nog steeds de oorspronkelijke array gebruikt. (Niet dat de IAP-wijziging van belang zou zijn geweest, zelfs als de array werd gedeeld.)

De tweede reeks voorbeelden had te maken met het gedrag van current() onder verschillende referentie / herconfiguratie configuraties. Dit is niet langer logisch, zoals current() is volledig onaangetast door de lus, dus de geretourneerde waarde blijft altijd hetzelfde.

We krijgen echter enkele interessante wijzigingen bij het overwegen van wijzigingen tijdens iteratie. Ik hoop dat je het nieuwe gedrag meer gezond zult vinden. Het eerste voorbeeld:

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

Zoals je ziet, breekt de buitenste lus niet meer af na de eerste iteratie. De reden is dat beide loops nu volledig gescheiden hashtable iterators hebben en er geen kruisbesmetting meer is van beide loops via een gedeelde IAP.

Een andere rare randcase die nu is opgelost, is het vreemde effect dat je krijgt wanneer je elementen verwijdert en toevoegt die dezelfde hasj hebben:

$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

Eerder sprong het HashPointer-herstelmechanisme direct naar het nieuwe element, omdat het "eruit zag" alsof het hetzelfde is als het verwijderelement (als gevolg van botsende hash en aanwijzer). Omdat we voor niets meer afhankelijk zijn van de element-hash, is dit niet langer een probleem.


1378
2018-02-13 13:21



In voorbeeld 3 wijzig je de array niet. In alle andere voorbeelden past u de inhoud of de interne matrixaanwijzer aan. Dit is belangrijk als het gaat om PHP matrices vanwege de semantiek van de toewijzingsoperator.

De opdrachtoperator voor de arrays in PHP werkt meer als een luie kloon. Het toewijzen van de ene variabele aan een andere die een array bevat, kloont de array, in tegenstelling tot de meeste talen. Het feitelijke klonen zal echter alleen plaatsvinden als dit nodig is. Dit betekent dat de kloon alleen zal plaatsvinden als een van de variabelen is gewijzigd (copy-on-write).

Hier is een voorbeeld:

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

Terugkomend op uw testcases, kunt u zich dat gemakkelijk voorstellen foreach maakt een soort iterator met een verwijzing naar de array. Deze referentie werkt precies zoals de variabele $b in mijn voorbeeld. Echter, de iterator samen met de referentie leven alleen tijdens de lus en vervolgens worden ze allebei weggegooid. Nu kun je zien dat, in alle gevallen behalve 3, de array tijdens de lus wordt aangepast, terwijl deze extra referentie levend is. Dit veroorzaakt een kloon, en dat verklaart wat hier gebeurt!

Hier is een uitstekend artikel voor een ander neveneffect van dit copy-on-write-gedrag: De PHP Ternary Operator: snel of niet?


97
2018-04-07 20:43



Enkele punten om op te letten bij het werken met foreach():

een) foreach werkt op de prospectieve kopie van de oorspronkelijke array.     Dit betekent dat foreach () een gedeelde gegevensopslag heeft tot of met een prospected copy is     niet gemaakt voor elke opmerking / opmerkingen van gebruikers.

b) Wat triggert een prospectieve kopie?     Prospected copy wordt gemaakt op basis van het beleid van copy-on-write, dat wil zeggen wanneer     een array die is doorgegeven aan foreach () is gewijzigd, een kloon van de oorspronkelijke array is gemaakt.

c) De originele array en foreach () iterator zullen hebben DISTINCT SENTINEL VARIABLES, dat wil zeggen, een voor de oorspronkelijke array en andere voor foreach; zie de testcode hieronder. SPL , iterators, en Array Iterator.

Stack Overflow-vraag Hoe zorg je ervoor dat de waarde wordt gereset in een 'foreach'-lus in PHP? behandelt de gevallen (3,4,5) van uw vraag.

Het volgende voorbeeld laat zien dat each () en reset () GEEN effect hebben SENTINEL variabelen (for example, the current index variable) van de foreach () iterator.

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

Output:

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



OPMERKING VOOR PHP 7

Dit antwoord bijwerken omdat het enige populariteit heeft gekregen: dit antwoord is niet langer van toepassing vanaf PHP 7. Zoals uitgelegd in de "Achterwaartse incompatibele wijzigingen", in PHP 7 voor elk werkt op kopie van de array, dus alle wijzigingen in de array zelf worden niet weerspiegeld in de foreach-lus. Meer details op de link.

Uitleg (citaat uit php.net):

De eerste vorm lus over de array gegeven door array_expression. Op elke   iteratie, wordt de waarde van het huidige element toegewezen aan $ waarde en   de interne array-wijzer wordt met één verhoogd (dus de volgende   iteratie, kijk je naar het volgende element).

In je eerste voorbeeld heb je dus maar één element in de array en wanneer de aanwijzer wordt verplaatst, bestaat het volgende element niet, dus nadat je nieuw element hebt toegevoegd voor elke einden, omdat het al "besliste" dat het het laatste element was.

In je tweede voorbeeld begin je met twee elementen en voor elke lus bevindt zich niet het laatste element, dus evalueert het de array in de volgende iteratie en realiseert dus dat er een nieuw element in de array is.

Ik geloof dat dit allemaal gevolg is van Op elke iteratie een deel van de uitleg in de documentatie, wat waarschijnlijk betekent dat foreach doet alle logica voordat het de code oproept {}.

Test geval

Als u dit uitvoert:

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

Je krijgt deze output:

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

Wat betekent dat het de wijziging accepteerde en er doorheen ging omdat het "op tijd" was gewijzigd. Maar als je dit doet:

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

Je zult krijgen:

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

Wat betekent dat array is gewijzigd, maar sinds we het hebben aangepast toen het foreach al was aan het laatste element van de array, het "besloten" om niet meer te lus, en hoewel we nieuw element toegevoegd, we voegde het toe "te laat" en het was niet doorgelust.

Gedetailleerde uitleg is te lezen op Hoe werkt PHP 'foreach' eigenlijk? wat de internals achter dit gedrag verklaart.


22
2018-04-15 08:46



Vanaf de documentatie verstrekt door PHP handleiding.

Op elke iteratie wordt de waarde van het huidige element toegewezen aan $ v en de interne waarde
  matrixaanwijzer wordt met één waarde vooruitgeschoven (dus bij de volgende iteratie kijk je naar het volgende element).

Dus volgens uw eerste voorbeeld:

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

$array hebben slechts één element, dus volgens de foreach-uitvoering, 1 toewijzen aan $ven het heeft geen ander element om de aanwijzer te verplaatsen

Maar in je tweede voorbeeld:

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

$array hebben twee elementen, dus nu evalueren $ array de nulindices en verplaatsen we de aanwijzer met één. Voor eerste iteratie van lus, toegevoegd $array['baz']=3; als referentie.


8
2018-04-15 09:32



Geweldige vraag, omdat veel ontwikkelaars, zelfs ervaren ontwikkelaars, in de war zijn door de manier waarop PHP arrays verwerkt in foreach-loops. In de standaard voor elke lus maakt PHP een kopie van de array die in de lus wordt gebruikt. De kopie wordt onmiddellijk verwijderd nadat de lus is voltooid. Dit is transparant in de werking van een eenvoudige foreach-lus. Bijvoorbeeld:

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

Dit levert:

apple
banana
coconut

Dus de kopie is aangemaakt maar de ontwikkelaar merkt het niet, omdat er niet naar de oorspronkelijke array wordt verwezen binnen de lus of nadat de lus is voltooid. Wanneer u echter probeert de items in een lus aan te passen, ziet u dat ze ongewijzigd zijn wanneer u klaar bent:

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

print_r($set);

Dit levert:

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

Wijzigingen van het origineel kunnen geen aankondigingen zijn, er zijn feitelijk geen wijzigingen ten opzichte van het origineel, ook al heeft u duidelijk een waarde toegekend aan $ artikel. Dit komt omdat u werkt op $ item zoals het wordt weergegeven in de kopie van $ set waaraan wordt gewerkt. Je kunt dit overschrijven door $ item te pakken door te verwijzen, zoals het volgende:

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

Dit levert:

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

Dus het is duidelijk en waarneembaar, wanneer $ item wordt beheerd door verwijzing, worden de wijzigingen in $ item gemaakt aan de leden van de originele $ set. Als u $ item bij referentie gebruikt, voorkomt u ook dat PHP de array-kopie maakt. Om dit te testen, zullen we eerst een snel script laten zien met de kopie:

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

Dit levert:

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

Zoals in het voorbeeld wordt getoond, heeft PHP $ set gekopieerd en gebruikt om door te lussen, maar toen de $ set in de lus werd gebruikt, voegde PHP de variabelen toe aan de oorspronkelijke array, niet aan de gekopieerde array. In principe gebruikt PHP alleen de gekopieerde array voor de uitvoering van de lus en de toewijzing van $ item. Daarom voert de bovenstaande lus slechts 3 keer uit en voegt elke keer een andere waarde toe aan het einde van de oorspronkelijke $ set, waarbij de oorspronkelijke $ -set met 6 elementen wordt achtergelaten, maar nooit een oneindige lus wordt ingevoerd.

Wat als we $ item bij referentie hadden gebruikt, zoals ik eerder al zei? Een enkel teken toegevoegd aan de bovenstaande test:

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

Resultaten in een oneindige lus. Merk op dat dit eigenlijk een oneindige lus is, je zult het script zelf moeten doden of moeten wachten tot je OS onvoldoende geheugen heeft. Ik voegde de volgende regel toe aan mijn script, dus PHP zou erg snel geheugen tekort komen, ik raad je aan hetzelfde te doen als je deze oneindige lus testen gaat uitvoeren:

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

Dus in dit vorige voorbeeld met de oneindige lus zien we de reden waarom PHP is geschreven om een ​​kopie van de array te creëren om door te lussen. Wanneer een kopie wordt gemaakt en alleen door de structuur van het lusconcept zelf wordt gebruikt, blijft de array gedurende de volledige uitvoering van de lus stabiel, zodat u nooit problemen tegenkomt.


5
2018-04-21 08:44



PHP voor elke lus kan worden gebruikt met Indexed arrays, Associative arrays en Object public variables.

In foreach-lus is het eerste dat php doet dat het een kopie maakt van de array die moet worden herhaald. PHP itereert dan over dit nieuwe copy van de array in plaats van de originele array. Dit wordt aangetoond in het onderstaande voorbeeld:

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

Daarnaast staat php het gebruik toe iterated values as a reference to the original array value ook. Dit wordt hieronder aangetoond:

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

Notitie: Het staat het niet toe original array indexes te gebruiken als references.

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


5
2017-11-13 14:08