Domanda In che modo PHP "foreach" funziona davvero?


Lasciatemi prefisso dicendo che so cosa foreach è, fa e come usarlo. Questa domanda riguarda il modo in cui funziona sotto il cofano e non voglio nessuna risposta sulla falsariga di "questo è il modo in cui si esegue il ciclo di un array con foreach".


Per molto tempo l'ho assunto foreach ha lavorato con la matrice stessa. Poi ho trovato molti riferimenti al fatto che funziona con a copia della matrice, e da allora ho assunto che questa sia la fine della storia. Ma di recente ho discusso la questione, e dopo un po 'di sperimentazione ho scoperto che non era vero al 100%.

Lascia che mostri cosa intendo. Per i seguenti casi di test, lavoreremo con il seguente 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 */

Questo dimostra chiaramente che non stiamo lavorando direttamente con l'array sorgente, altrimenti il ​​ciclo continuerebbe per sempre, dal momento che stiamo costantemente spingendo gli elementi sull'array durante il ciclo. Ma solo per essere sicuri che questo è il caso:

Test case 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 */

Ciò conferma la nostra conclusione iniziale, stiamo lavorando con una copia dell'array sorgente durante il ciclo, altrimenti vedremmo i valori modificati durante il ciclo. Ma...

Se guardiamo nel Manuale, troviamo questa affermazione:

Quando foreach viene avviato per la prima volta, il puntatore dell'array interno viene automaticamente reimpostato sul primo elemento dell'array.

Giusto ... questo sembra suggerirlo foreach si basa sul puntatore dell'array dell'array sorgente. Ma abbiamo appena dimostrato di esserlo non funziona con l'array sorgente, destra? Bene, non interamente.

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

Quindi, nonostante non lavoriamo direttamente con l'array sorgente, stiamo lavorando direttamente con il puntatore dell'array sorgente: il fatto che il puntatore si trovi alla fine dell'array alla fine del loop lo mostra. Tranne che questo non può essere vero - se lo fosse, allora test case 1 farebbe un ciclo per sempre.

Il manuale PHP afferma inoltre:

Siccome foreach si basa sul puntatore dell'array interno cambiandolo all'interno del loop può portare a comportamenti imprevisti.

Bene, scopriamo cos'è questo "comportamento inaspettato" (tecnicamente, qualsiasi comportamento è inaspettato poiché non so più cosa aspettarmi).

Test case 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 */

... niente di così inaspettato, in effetti sembra supportare la teoria della "copia della fonte".


La domanda

Che cosa sta succedendo qui? Il mio C-fu non è abbastanza buono per me da poter estrarre una conclusione corretta semplicemente guardando il codice sorgente PHP, sarei grato se qualcuno potesse tradurlo in inglese per me.

Mi sembra questo foreach funziona con a copia dell'array, ma imposta il puntatore dell'array dell'array sorgente alla fine dell'array dopo il loop.

  • È corretto e l'intera storia?
  • Se no, cosa sta facendo davvero?
  • C'è qualche situazione in cui si usano le funzioni che regolano il puntatore dell'array (each(), reset() et al.) durante a foreach potrebbe influire sul risultato del ciclo?

1637
2018-04-07 19:33


origine


risposte:


foreach supporta l'iterazione su tre diversi tipi di valori:

Di seguito proverò a spiegare precisamente come funziona l'iterazione nei diversi casi. Di gran lunga il caso più semplice sono Traversable oggetti, come per questi foreach è essenzialmente solo lo zucchero di sintassi per il codice lungo queste linee:

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

Per le classi interne, le chiamate al metodo effettivo vengono evitate utilizzando un'API interna che essenzialmente rispecchia solo le Iterator interfaccia al livello C.

L'iterazione di matrici e oggetti semplici è significativamente più complicata. Prima di tutto, va notato che in PHP gli "array" sono dizionari veramente ordinati e verranno attraversati secondo questo ordine (che corrisponde all'ordine di inserimento purché non si utilizzi qualcosa come sort). Ciò si oppone all'iterazione dell'ordine naturale delle chiavi (come spesso funzionano gli elenchi in altre lingue) o non ha alcun ordine definito (come spesso funzionano i dizionari in altre lingue).

Lo stesso vale anche per gli oggetti, in quanto le proprietà dell'oggetto possono essere viste come un altro dizionario (ordinato) che associa i nomi delle proprietà ai loro valori, oltre a un po 'di gestione della visibilità. Nella maggior parte dei casi le proprietà dell'oggetto non vengono effettivamente memorizzate in questo modo piuttosto inefficiente. Tuttavia, se inizi a scorrere su un oggetto, la rappresentazione compressa che viene normalmente utilizzata verrà convertita in un dizionario reale. A quel punto, l'iterazione di oggetti semplici diventa molto simile all'iterazione di matrici (che è il motivo per cui non sto discutendo l'iterazione degli oggetti semplici molto qui).

Fin qui tutto bene. L'iterazione su un dizionario non può essere troppo difficile, giusto? I problemi iniziano quando ti rendi conto che un array / oggetto può cambiare durante l'iterazione. Ci sono molti modi in cui ciò può accadere:

  • Se si itera per riferimento usando foreach ($arr as &$v) poi $arr diventa un riferimento e puoi cambiarlo durante l'iterazione.
  • In PHP 5 vale lo stesso anche se si esegue iterazione in base al valore, ma l'array era un riferimento in precedenza: $ref =& $arr; foreach ($ref as $v)
  • Gli oggetti hanno una semantica passante, che per scopi pratici significa che si comportano come riferimenti. Quindi gli oggetti possono sempre essere modificati durante l'iterazione.

Il problema con l'autorizzazione delle modifiche durante l'iterazione è il caso in cui l'elemento attualmente in uso viene rimosso. Supponiamo che tu usi un puntatore per tenere traccia di quale elemento dell'array ti trovi attualmente. Se questo elemento è ora liberato, si rimane con un puntatore pendente (di solito risultante in un segfault).

Ci sono diversi modi per risolvere questo problema. PHP 5 e PHP 7 differiscono significativamente a questo proposito e descriverò entrambi i comportamenti nel seguito. Il riassunto è che l'approccio di PHP 5 era piuttosto stupido e portava a tutti i tipi di problemi di caso marginali, mentre l'approccio più implicito di PHP 7 risulta in un comportamento più prevedibile e coerente.

Come ultimo preliminare, va notato che PHP utilizza il conteggio dei riferimenti e il copy-on-write per gestire la memoria. Ciò significa che se si "copia" un valore, in realtà si riutilizza il vecchio valore e si incrementa il suo conteggio di riferimento (refcount). Solo dopo aver eseguito una sorta di modifica verrà eseguita una copia reale (chiamata "duplicazione"). Vedere Ti stanno mentendo per una più ampia introduzione su questo argomento.

PHP 5

Puntatore dell'array interno e HashPointer

Le matrici in PHP 5 hanno un "puntatore dell'array interno" dedicato (IAP), che supporta correttamente le modifiche: Ogni volta che un elemento viene rimosso, ci sarà un controllo se l'IAP punta a questo elemento. Se lo fa, è invece avanzato all'elemento successivo.

Mentre foreach fa uso di IAP, c'è un'ulteriore complicazione: c'è solo un IAP, ma un array può essere parte di più cicli foreach:

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

Per supportare due loop simultanei con un solo puntatore array interno, foreach esegue i seguenti schenanigans: prima che il corpo del ciclo sia eseguito, foreach eseguirà il backup di un puntatore all'elemento corrente e il relativo hash in un for-foreach HashPointer. Dopo che il corpo del ciclo è stato eseguito, lo IAP verrà reimpostato su questo elemento se esiste ancora. Se tuttavia l'elemento è stato rimosso, utilizzeremo solo il punto in cui si trova attualmente l'IAP. Questo schema è per lo più di tipo kinda, ma c'è un sacco di strani comportamenti da cui puoi uscirne, alcuni dei quali mostrerò qui sotto.

Duplicazione di matrice

Lo IAP è una caratteristica visibile di un array (esposto attraverso il current famiglia di funzioni), in quanto tali modifiche allo IAP vengono considerate come modifiche nella semantica copy-on-write. Questo purtroppo significa che foreach è in molti casi costretto a duplicare l'array su cui sta iterando. Le condizioni precise sono:

  1. La matrice non è un riferimento (is_ref = 0). Se si tratta di un riferimento, quindi le modifiche ad esso sono ipotetico propagare, quindi non dovrebbe essere duplicato.
  2. L'array ha un conteggio> 1. Se refcount è 1, la matrice non è condivisa e siamo liberi di modificarla direttamente.

Se l'array non è duplicato (is_ref = 0, refcount = 1), verrà incrementato solo il suo refcount (*). Inoltre, se viene utilizzato foreach per riferimento, la matrice (potenzialmente duplicata) verrà trasformata in riferimento.

Considera questo codice come un esempio in cui si verifica la duplicazione:

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

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

Qui, $arr sarà duplicato per impedire le modifiche IAP su $arr da perdite a $outerArr. In termini delle condizioni di cui sopra, l'array non è un riferimento (is_ref = 0) e viene utilizzato in due posizioni (refcount = 2). Questo requisito è sfortunato e un artefatto dell'implementazione subottimale (non ci sono problemi di modifica durante l'iterazione qui, quindi non abbiamo davvero bisogno di usare l'IAP in primo luogo).

(*) Incrementare il conto qui sembra innocuo, ma viola la semantica copy-on-write (COW): questo significa che stiamo andando a modificare l'IAP di un refcount = 2 array, mentre COW detta che le modifiche possono essere eseguite solo su refcount = 1 valori. Questa violazione comporta una modifica del comportamento visibile all'utente (mentre COW è normalmente trasparente), poiché la modifica IAP sull'array iterato sarà osservabile, ma solo fino alla prima modifica non IAP sull'array. Invece, le tre opzioni "valide" avrebbero dovuto essere a) duplicare sempre, b) non incrementare il conto e permettere così che l'array iterato fosse arbitrariamente modificato nel ciclo, o c) non usare affatto l'IAP ( la soluzione PHP 7).

Ordine di avanzamento posizione

C'è un ultimo dettaglio di implementazione di cui devi essere a conoscenza per comprendere correttamente i seguenti esempi di codice. Il modo "normale" di eseguire il looping di alcune strutture dati sarebbe simile a questo in pseudocodice:

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

però foreach, essendo un fiocco di neve piuttosto speciale, sceglie di fare le cose in modo leggermente diverso:

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

Vale a dire, il puntatore dell'array è già stato spostato in avanti prima il corpo del ciclo scorre. Ciò significa che mentre il corpo del loop sta lavorando sull'elemento $i, lo IAP è già a elemento $i+1. Questo è il motivo per cui i campioni di codice che mostrano la modifica durante l'iterazione disattiveranno sempre Il prossimo elemento, piuttosto che quello attuale.

Esempi: i tuoi casi di test

I tre aspetti descritti sopra dovrebbero fornire un'impressione per lo più completa delle idiosincrasie dell'implementazione di foreach e possiamo passare alla discussione di alcuni esempi.

Il comportamento dei casi di test è semplice da spiegare a questo punto:

  • Nei casi di test 1 e 2 $array inizia con refcount = 1, quindi non verrà duplicato da foreach: solo il conto è incrementato. Quando il corpo del ciclo modifica di conseguenza l'array (che ha refcount = 2 in quel punto), la duplicazione si verificherà in quel punto. Foreach continuerà a lavorare su una copia non modificata di $array.

  • Nel caso di test 3, ancora una volta l'array non è duplicato, quindi foreach modificherà l'IAP del $array variabile. Alla fine dell'iterazione, lo IAP è NULL (ovvero l'iterazione eseguita), che each indica restituendo false.

  • Nei casi di test 4 e 5 entrambi each e reset sono funzioni di riferimento. Il $array ha un refcount=2 quando viene passato a loro, quindi deve essere duplicato. Come tale foreach lavorerà di nuovo su un array separato.

Esempi: effetti di current in foreach

Un buon modo per mostrare i vari comportamenti di duplicazione è osservare il comportamento di current() funzione all'interno di un ciclo foreach. Considera questo esempio:

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

Qui dovresti saperlo current() è una funzione by-ref (in realtà: prefer-ref), anche se non modifica l'array. Deve essere buono per giocare con tutte le altre funzioni come next che sono tutti di riferimento. Il passaggio per riferimento implica che l'array deve essere separato e quindi $array e l'array foreach sarà diverso. Il motivo per cui ottieni 2 invece di 1 è anche menzionato sopra: foreach fa avanzare il puntatore dell'array prima eseguendo il codice utente, non dopo. Quindi, anche se il codice è al primo elemento, foreach ha già avanzato il puntatore al secondo.

Ora proviamo una piccola modifica:

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

Qui abbiamo il caso is_ref = 1, quindi l'array non viene copiato (come sopra). Ma ora che si tratta di un riferimento, l'array non deve più essere duplicato quando si passa al by-ref current() funzione. così current() e foreach lavoro sullo stesso array. Tuttavia, a causa del modo in cui vedi, tuttavia, il comportamento è fuori dalla norma foreachavanza il puntatore.

Si ottiene lo stesso comportamento quando si esegue l'iterazione per ref:

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

Qui la parte importante è quella che foreach farà $array un is_ref = 1 quando viene iterato per riferimento, quindi in pratica si ha la stessa situazione di cui sopra.

Un'altra piccola variazione, questa volta assegneremo la matrice a un'altra variabile:

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

Qui il conto del $array è 2 quando viene avviato il ciclo, quindi per una volta dobbiamo fare la duplicazione in anticipo. così $array e la matrice utilizzata da foreach sarà completamente separata dall'inizio. Ecco perché ottieni la posizione dello IAP ovunque si trovasse prima del ciclo (in questo caso era nella prima posizione).

Esempi: modifica durante l'iterazione

Il tentativo di rendere conto delle modifiche durante l'iterazione è il punto in cui sono originati tutti i nostri problemi di foreach, quindi serve considerare alcuni esempi per questo caso.

Considera questi cicli annidati sullo stesso array (dove viene usata l'iterazione per ref per assicurarsi che sia davvero la stessa):

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)

La parte prevista qui è quella (1, 2) manca l'output, perché elemento 1 è stato rimosso. Ciò che probabilmente è inaspettato è che il ciclo esterno si arresta dopo il primo elemento. Perché?

Il motivo alla base di questo è l'hack del ciclo nested descritto in precedenza: prima che il corpo del ciclo venga eseguito, l'attuale posizione e hash dello IAP vengono copiati in un HashPointer. Dopo il corpo del ciclo verrà ripristinato, ma solo se l'elemento esiste ancora, altrimenti viene utilizzata la posizione corrente dello IAP (qualunque essa sia). Nell'esempio sopra questo è esattamente il caso: l'elemento corrente del ciclo esterno è stato rimosso, quindi utilizzerà l'IAP, che è già stato contrassegnato come finito dal ciclo interno!

Un'altra conseguenza del HashPointer il meccanismo di backup + ripristino è comunque quello che modifica lo IAP reset() ecc. di solito non influenzano il foreach. Ad esempio, il codice seguente viene eseguito come se il reset() non erano affatto presenti:

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

Il motivo è che, mentre reset() modifica temporaneamente lo IAP, verrà ripristinato nell'elemento foreach corrente dopo il corpo del ciclo. Forzare reset() per rendere un effetto sul ciclo, è necessario rimuovere anche l'elemento corrente, in modo che il meccanismo di backup / ripristino non riesca:

$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

Ma quegli esempi sono ancora sani. Il vero divertimento inizia se ti ricordi che il HashPointer restore usa un puntatore all'elemento e il suo hash per determinare se esiste ancora. Ma: gli hash hanno collisioni e i puntatori possono essere riutilizzati! Ciò significa che, con un'attenta scelta dei tasti dell'array, possiamo fare foreach crediamo che un elemento che è stato rimosso esiste ancora, quindi salterà direttamente ad esso. Un esempio:

$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

Qui dovremmo normalmente aspettarci l'output 1, 1, 3, 4 secondo le regole precedenti. Come succede quello 'FYFY' ha lo stesso hash dell'elemento rimosso 'EzFY'e l'allocatore capita di riutilizzare la stessa posizione di memoria per memorizzare l'elemento. Quindi foreach finisce direttamente saltando sull'elemento appena inserito, quindi taglia corto il ciclo.

Sostituendo l'entità iterata durante il ciclo

Un ultimo caso strano che vorrei menzionare, è che PHP consente di sostituire l'entità iterata durante il ciclo. Quindi è possibile iniziare a scorrere su un array e quindi sostituirlo con un altro array a metà. Oppure inizia a scorrere su un array e poi sostituirlo con un oggetto:

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

Come puoi vedere in questo caso, PHP inizierà semplicemente a iterare l'altra entità dall'inizio dopo che la sostituzione è avvenuta.

PHP 7

Iteratori Hashtable

Se si ricorda ancora, il problema principale con l'iterazione dell'array era come gestire la rimozione degli elementi a metà iterazione. PHP 5 utilizzava un singolo puntatore a matrice interna (IAP) per questo scopo, che era alquanto subottimale, poiché un puntatore a matrice doveva essere allungato per supportare più cicli foreach simultanei e interazione con reset() ecc. in aggiunta.

PHP 7 utilizza un approccio diverso, ovvero supporta la creazione di una quantità arbitraria di iteratori hashtable esterni e sicuri. Questi iteratori devono essere registrati nell'array, da cui hanno la stessa semantica della IAP: se un elemento dell'array viene rimosso, tutti gli iteratori di hash che puntano a quell'elemento saranno passati all'elemento successivo.

Ciò significa che foreach non utilizzerà più lo IAP affatto. Il ciclo foreach non avrà assolutamente alcun effetto sui risultati di current() ecc. e il suo comportamento non sarà mai influenzato da funzioni come reset() eccetera.

Duplicazione di matrice

Un altro importante cambiamento tra PHP 5 e PHP 7 riguarda la duplicazione di array. Ora che l'IAP non viene più utilizzato, l'iterazione dell'array in base al valore eseguirà un incremento del conteggio (anziché la duplicazione dell'array) in tutti i casi. Se la matrice viene modificata durante il ciclo foreach, a quel punto si verificherà una duplicazione (in base al copy-on-write) e foreach continuerà a funzionare sul vecchio array.

Nella maggior parte dei casi questo cambiamento è trasparente e non ha altri effetti se non una migliore prestazione. Tuttavia, c'è un'occasione in cui si verifica un comportamento diverso, vale a dire il caso in cui la matrice era un riferimento in precedenza:

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

In precedenza l'iterazione di valore degli array di riferimento era un caso speciale. In questo caso non si è verificata alcuna duplicazione, quindi tutte le modifiche dell'array durante l'iterazione verranno riflesse dal loop. In PHP 7 questo caso speciale è scomparso: verrà eseguita un'iterazione per valore di un array sempre continua a lavorare sugli elementi originali, ignorando eventuali modifiche durante il ciclo.

Questo, ovviamente, non si applica all'iterazione di riferimento. Se si itera un riferimento per riferimento, tutte le modifiche verranno riflesse dal ciclo. È interessante notare che lo stesso vale per l'iterazione in base al valore di oggetti semplici:

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

Ciò riflette la semantica degli oggetti gestita (vale a dire che si comportano come riferimento anche in contesti di valore).

Esempi

Prendiamo in considerazione alcuni esempi, a partire dai casi di test:

  • I casi di test 1 e 2 mantengono lo stesso risultato: l'iterazione dell'array By-value continua a funzionare sugli elementi originali. (In questo caso anche il conteggio e il comportamento di duplicazione sono esattamente gli stessi tra PHP 5 e PHP 7).

  • Test case 3 modifiche: Foreach non utilizza più l'IAP, quindi each() non è influenzato dal ciclo. Avrà la stessa uscita prima e dopo.

  • I casi di test 4 e 5 rimangono gli stessi: each() e reset() duplicherà l'array prima di modificare l'IAP, mentre foreach utilizza ancora l'array originale. (Non che il cambiamento IAP avrebbe avuto importanza, anche se l'array era condiviso).

La seconda serie di esempi era correlata al comportamento di current() sotto diverse configurazioni di riferimento / riconversione. Questo non ha più senso, come current() non è completamente influenzato dal ciclo, quindi il suo valore di ritorno rimane sempre lo stesso.

Tuttavia, otteniamo alcuni cambiamenti interessanti quando si considerano le modifiche durante l'iterazione. Spero che troverai il nuovo comportamento più sano. Il primo esempio:

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

Come puoi vedere, il ciclo esterno non si interrompe più dopo la prima iterazione. Il motivo è che entrambi i loop ora hanno iteratori di hashtable completamente separati e non esiste più alcuna contaminazione incrociata di entrambi i loop attraverso uno IAP condiviso.

Un altro caso di bordo strano che è stato risolto ora è l'effetto dispari che ottieni quando rimuovi e aggiungi elementi che hanno lo stesso hash:

$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

In precedenza, il meccanismo di ripristino di HashPointer saltava direttamente al nuovo elemento, perché "sembrava" come se fosse l'elemento di rimozione (a causa di collisione dell'hash e del puntatore). Poiché non ci basiamo più sull'hash dell'elemento per nulla, questo non è più un problema.


1378
2018-02-13 13:21



Nell'esempio 3 non si modifica la matrice. In tutti gli altri esempi si modifica il contenuto o il puntatore dell'array interno. Questo è importante quando si tratta di PHP array a causa della semantica dell'operatore di assegnazione.

L'operatore di assegnazione per gli array in PHP funziona più come un clone pigro. Assegnare una variabile a un'altra che contiene una matrice clonerà la matrice, a differenza della maggior parte delle lingue. Tuttavia, la clonazione effettiva non verrà eseguita a meno che non sia necessaria. Ciò significa che il clone avverrà solo quando una delle variabili viene modificata (copy-on-write).

Ecco un esempio:

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

Tornando ai tuoi casi di test, puoi facilmente immaginarlo foreach crea qualche tipo di iteratore con un riferimento alla matrice. Questo riferimento funziona esattamente come la variabile $b nel mio esempio Tuttavia, l'iteratore e il riferimento vengono visualizzati solo durante il ciclo e quindi vengono entrambi scartati. Ora puoi vedere che, in tutti i casi tranne 3, la matrice viene modificata durante il ciclo, mentre questo riferimento extra è vivo. Questo fa scattare un clone e questo spiega cosa sta succedendo qui!

Ecco un eccellente articolo per un altro effetto collaterale di questo comportamento copy-on-write: L'operatore ternario PHP: veloce o no?


97
2018-04-07 20:43



Alcuni punti da notare quando si lavora con foreach():

un) foreach lavora su copia prospettata della matrice originale.     Significa che foreach () avrà SHARED data storage fino ao meno che a prospected copy è     non creato note foreach / commenti degli utenti.

b) Cosa fa scattare a copia prospettata?     La copia prospettica è creata in base alla politica di copy-on-write, cioè, ogni volta     una matrice passata a foreach () viene modificata, viene creato un clone dell'array originale.

c) L'array originale e l'iteratore foreach () avranno DISTINCT SENTINEL VARIABLES, cioè, uno per l'array originale e altro per foreach; guarda il codice di prova qui sotto. SPL , iteratori, e Array Iterator.

Stack Overflow question Come assicurarsi che il valore sia resettato in un ciclo 'foreach' in PHP? affronta i casi (3,4,5) della tua domanda.

L'esempio seguente mostra che each () e reset () NON hanno effetto SENTINEL variabili (for example, the current index variable) del iteratore foreach ().

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

Produzione:

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



NOTA PER PHP 7

Per aggiornare questa risposta in quanto ha guadagnato un po 'di popolarità: questa risposta non si applica più a partire da PHP 7. Come spiegato nella sezione "Modifiche all'indietro incompatibili", in PHP 7 foreach funziona sulla copia dell'array, quindi eventuali modifiche sull'array stesso non si riflettono sul ciclo foreach. Maggiori dettagli al link.

Spiegazione (citazione da php.net):

Il primo modulo esegue il loop sull'array dato da array_expression. Su ciascun   iterazione, il valore dell'elemento corrente è assegnato a $ valore e   il puntatore dell'array interno è avanzato di uno (quindi sul prossimo)   iterazione, vedrai il prossimo elemento).

Quindi, nel tuo primo esempio hai solo un elemento nell'array, e quando il puntatore viene spostato, l'elemento successivo non esiste, quindi dopo aver aggiunto il nuovo elemento foreach finisce perché già "decide" che è l'ultimo elemento.

Nel secondo esempio, si inizia con due elementi e il ciclo foreach non si trova nell'ultimo elemento, quindi valuta l'array alla successiva iterazione e quindi si rende conto che c'è un nuovo elemento nell'array.

Credo che questa sia una conseguenza di Ad ogni iterazione parte della spiegazione nella documentazione, che probabilmente significa questo foreach fa tutta la logica prima che chiama il codice in {}.

Test case

Se esegui questo:

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

Otterrai questo risultato:

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

Il che significa che ha accettato la modifica e l'ha esaminata perché è stata modificata "in tempo". Ma se lo fai:

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

Otterrete:

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

Il che significa che l'array è stato modificato, ma poiché lo abbiamo modificato quando il file foreach era già all'ultimo elemento dell'array, "ha deciso" di non effettuare più il ciclo e, anche se abbiamo aggiunto un nuovo elemento, lo abbiamo aggiunto "troppo tardi" e non è stato eseguito il ciclo.

Una spiegazione dettagliata può essere letta a In che modo PHP "foreach" funziona davvero? che spiega gli interni di questo comportamento.


22
2018-04-15 08:46



Secondo la documentazione fornita dal manuale PHP.

Ad ogni iterazione, il valore dell'elemento corrente viene assegnato a $ v e l'interno
  il puntatore dell'array è avanzato di uno (quindi nella prossima iterazione vedrai il prossimo elemento).

Quindi, come per il tuo primo esempio:

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

$array avere solo un singolo elemento, così come per l'esecuzione di foreach, 1 assegnare a $ve non ha nessun altro elemento per spostare il puntatore

Ma nel tuo secondo esempio:

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

$array hanno due elementi, quindi ora $ array valuta gli indici zero e sposta il puntatore di uno. Per prima iterazione del ciclo, aggiunto $array['baz']=3; come passare per riferimento.


8
2018-04-15 09:32



Grande domanda, perché molti sviluppatori, anche esperti, sono confusi dal modo in cui PHP gestisce gli array nei cicli foreach. Nel ciclo foreach standard, PHP crea una copia dell'array utilizzato nel ciclo. La copia viene scartata immediatamente dopo la fine del ciclo. Questo è trasparente nel funzionamento di un ciclo foreach semplice. Per esempio:

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

Questo produce:

apple
banana
coconut

Quindi la copia viene creata ma lo sviluppatore non se ne accorge, poiché l'array originale non viene referenziato all'interno del ciclo o dopo che il ciclo termina. Tuttavia, quando si tenta di modificare gli elementi in un ciclo, si scopre che non sono modificati al termine:

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

print_r($set);

Questo produce:

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

Eventuali modifiche dall'originale non possono essere note, in realtà non ci sono modifiche rispetto all'originale, anche se hai chiaramente assegnato un valore a $ item. Questo perché stai operando su $ item come appare nella copia di $ set su cui si sta lavorando. Puoi sovrascriverlo afferrando $ item per riferimento, in questo modo:

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

Questo produce:

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

Quindi è evidente e osservabile, quando $ item viene gestito per riferimento, le modifiche apportate a $ item vengono apportate ai membri del set $ originale. L'utilizzo di $ item per riferimento impedisce inoltre a PHP di creare la copia dell'array. Per testare questo, prima mostreremo uno script veloce che dimostra la copia:

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

Questo produce:

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

Come è mostrato nell'esempio, PHP ha copiato $ set e lo ha usato per il loopover, ma quando $ set è stato usato all'interno del loop, PHP ha aggiunto le variabili all'array originale, non all'array copiato. Fondamentalmente, PHP usa solo l'array copiato per l'esecuzione del ciclo e l'assegnazione di $ item. Per questo motivo, il ciclo precedente viene eseguito solo 3 volte e ogni volta aggiunge un altro valore alla fine del set $ originale, lasciando il set $ originale con 6 elementi, ma senza mai entrare in un ciclo infinito.

Tuttavia, cosa succede se abbiamo usato $ item per riferimento, come ho detto prima? Un singolo carattere aggiunto al test precedente:

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

Risultati in un ciclo infinito. Nota che in realtà si tratta di un ciclo infinito, dovrai uccidere lo script o attendere che il tuo sistema operativo esaurisca la memoria. Ho aggiunto la seguente riga al mio script in modo che PHP possa esaurire la memoria molto rapidamente, ti suggerisco di fare lo stesso se stai per eseguire questi test a ciclo infinito:

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

Quindi, in questo precedente esempio con il ciclo infinito, vediamo il motivo per cui PHP è stato scritto per creare una copia dell'array su cui eseguire il loop. Quando una copia viene creata e utilizzata solo dalla struttura del costrutto loop stesso, l'array rimane statico durante l'esecuzione del ciclo, quindi non si verificheranno mai problemi.


5
2018-04-21 08:44



Il ciclo foreach di PHP può essere utilizzato con Indexed arrays, Associative arrays e Object public variables.

Nel ciclo foreach, la prima cosa che fa php è che crea una copia dell'array su cui deve essere ripetuta l'iterazione. PHP quindi itera su questo nuovo copy della matrice piuttosto che quella originale. Questo è dimostrato nell'esempio seguente:

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

Oltre a questo, php consente di utilizzare iterated values as a reference to the original array value anche. Questo è dimostrato di seguito:

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

Nota: Non consente original array indexes essere usato come references.

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


5
2017-11-13 14:08