it-swarm-eu.dev

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 alcuna risposta sulla falsariga di "questo è il modo in cui si esegue il ciclo di un array con foreach".


Per molto tempo ho pensato che foreach funzionasse con l'array stesso. Poi ho trovato molti riferimenti al fatto che funziona con una copia dell'array, e da allora ho assunto che questa fosse 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 continuamente 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 suggerire che foreach si appoggi al puntatore dell'array dell'array sorgente. Ma abbiamo appena dimostrato che siamo non funziona con l'array sorgente , giusto? 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 il test case 1 sarebbe loop per sempre.

Il manuale PHP afferma anche:

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 per me da poter estrarre una conclusione corretta semplicemente guardando il codice sorgente diPHP, sarei grato se qualcuno potesse tradurlo in inglese per me.

Mi sembra che foreach funzioni con una copia dell'array, ma imposta il puntatore dell'array dell'array sorgente alla fine dell'array dopo il ciclo.

  • È corretto e l'intera storia?
  • Se no, cosa sta facendo davvero?
  • C'è qualche situazione in cui l'utilizzo di funzioni che regolano il puntatore dell'array (each(), reset() et al.) Durante un foreach potrebbe influenzare il risultato del ciclo?
1839
DaveRandom

foreachsupporta l'iterazione su tre diversi tipi di valori:

Di seguito, proverò a spiegare precisamente come funziona l'iterazione in diversi casi. Di gran lunga il caso più semplice è Traversableoggetti, in quanto 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 riflette solo l'interfaccia Iteratoral 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 usi 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 funzionano spesso 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. Iterare 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 esegue iterazione per riferimento usando foreach ($arr as &$v) allora $arr diventa un riferimento e si può cambiarlo durante l'iterazione.
  • In PHP 5 lo stesso si applica 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 di passaggio gestita, 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 in cui ci si trova attualmente 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 modo seguente. Il riassunto è che l'approccio di PHP 5 era piuttosto stupido e portava a tutti i tipi di problemi di Edge problematici, mentre PHP 7 comportava un approccio più coinvolgente con un comportamento più prevedibile e coerente.

Come ultimo preliminare, si dovrebbe notare che PHP usa il conteggio dei riferimenti e la copia su scrittura per gestire la memoria. Ciò significa che se si "copia" un valore, in realtà si riutilizza il vecchio valore e si incrementa il conteggio dei riferimenti (refcount). Solo dopo aver eseguito una sorta di modifica verrà eseguita una copia reale (chiamata "duplicazione"). Vedi Stai mentendo a per un'introduzione più ampia su questo argomento.

PHP 5

Puntatore dell'array interno e HashPointer

Le matrici in PHP 5 hanno un "puntatore di matrice 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 foreachfa uso di IAP, c'è un'ulteriore complicazione: c'è solo un IAP, ma un array può essere parte di più loop foreachname__:

// 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 dell'array interno, foreachesegue i seguenti shenanigans: Prima che il corpo del ciclo venga eseguito, foreacheseguirà il backup di un puntatore all'elemento corrente e il relativo hash in un HashPointerper-foreach. 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 l'attuale IAP. Questo schema è per lo più un tipo di lavoro, ma c'è un sacco di strani comportamenti che puoi trarne, alcuni dei quali mostrerò di seguito.

Duplicazione di matrice

Lo IAP è una caratteristica visibile di un array (esposto attraverso la famiglia di funzioni currentname__), in quanto tali modifiche al conteggio IAP come modifiche nella semantica copy-on-write. Questo, sfortunatamente, 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, le modifiche ad esso sono supposto da 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), solo il suo refcountverrà incrementato (*). Inoltre, se viene utilizzato foreachper 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($outerArr);

Qui, $arr verrà duplicato per impedire che le modifiche IAP su $arr provengano da $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 utilizzare l'IAP in primo luogo).

(*) L'incremento di refcountqui 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 un COW è normalmente trasparente) perché la modifica IAP sull'array iterato sarà osservabile, ma solo fino alla prima modifica non IAP sull'array. Invece, le tre opzioni "valide" sarebbero state a) da duplicare sempre, b) non incrementare refcounte quindi consentire all'array iterato di essere arbitrariamente modificato nel ciclo o c) non utilizzare affatto l'IAP (il PHP 7 soluzione).

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

Tuttavia foreachname__, 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 che il corpo del ciclo venga eseguito. Ciò significa che mentre il corpo del ciclo sta lavorando sull'elemento $i, lo IAP è già all'elemento $i+1. Questo è il motivo per cui gli esempi di codice che mostrano la modifica durante l'iterazione saranno sempre unsetl'elemento successivo , anziché quello corrente.

Esempi: i tuoi casi di test

I tre aspetti descritti sopra dovrebbero fornire un'impressione per lo più completa delle idiosincrasie dell'implementazione foreache possiamo passare a discutere 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 sarà duplicato da foreachname__: solo il refcountviene 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 foreachmodificherà lo IAP della variabile $array. Alla fine dell'iterazione, l'IAP è NULL (il che significa che l'iterazione è stata eseguita), che eachindica restituendo falsename__.

  • Nei casi di test 4 e 5 sia eachche resetsono funzioni di riferimento. Il $array ha un refcount=2 quando viene passato a loro, quindi deve essere duplicato. In quanto tale foreachlavorerà nuovamente su un array separato.

Esempi: Effetti di currentin foreach

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

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

Qui dovresti sapere che current() è una funzione by-ref (in realtà: prefer-ref), anche se non modifica l'array. Deve essere al fine di giocare a Nizza con tutte le altre funzioni come nextche sono tutte di riferimento. Il passaggio per riferimento implica che l'array deve essere separato e quindi $array e foreach-array saranno diversi. Il motivo per cui ottieni 2 invece di 1 è anche menzionato sopra: foreachanticipa il puntatore dell'array prima che esegue il codice utente, non dopo. Quindi, anche se il codice è al primo elemento, foreachha 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 la matrice non viene copiata (come sopra). Ma ora che si tratta di un riferimento, l'array non deve più essere duplicato quando si passa alla funzione by-ref current(). Quindi current() e foreachfunzionano sullo stesso array. Si vede comunque il comportamento off-by, a causa del modo in cui 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 è che foreach renderà $array an is_ref = 1 quando viene iterato per riferimento, quindi in pratica hai 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 conteggio di $array è 2 quando viene avviato il ciclo, quindi per una volta dobbiamo fare la duplicazione in anticipo. Pertanto $array e l'array utilizzato da foreach saranno completamente separati 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

Cercando di spiegare le modifiche durante l'iterazione è dove 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 utilizzata 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 è che (1, 2) manca nell'output perché l'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 sopra: Prima che il corpo del ciclo venga eseguito, la posizione e l'hash corrente dell'happ vengono sottoposti a backup in un HashPointername__. Dopo il corpo del loop verrà ripristinato, ma solo se l'elemento esiste ancora, altrimenti viene utilizzata la posizione corrente di IAP (qualunque essa sia). Nell'esempio sopra questo è esattamente il caso: l'elemento corrente del loop esterno è stato rimosso, quindi utilizzerà l'IAP, che è già stato contrassegnato come finito dal loop interno!

Un'altra conseguenza del meccanismo di backup + ripristino HashPointerè che le modifiche allo IAP sebbene reset() ecc. Di solito non influiscono su foreachname__. Ad esempio, il codice seguente viene eseguito come se reset() non fosse affatto presente:

$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. Per forzare reset() a effettuare un effetto sul ciclo, devi rimuovere ulteriormente 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 ripristino HashPointerutilizza 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 delle chiavi di array, possiamo rendere foreachcredendo 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 ciò che accade è che '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 verso l'elemento appena inserito, quindi tagliare il ciclo in breve.

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 ripetere 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 a tal fine un singolo puntatore a matrice interna (IAP), che era in qualche modo subottimale, poiché un puntatore di array 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 foreachnon utilizzerà più l'IAP affatto . Il ciclo foreachnon avrà assolutamente alcun effetto sui risultati di current() ecc. E il suo comportamento non sarà mai influenzato da funzioni come reset() ecc.

Duplicazione di matrice

Un altro cambiamento importante tra PHP 5 e PHP 7 si riferisce alla duplicazione di array. Ora che l'IAP non viene più utilizzato, l'iterazione dell'array by-value eseguirà solo un incremento refcount(anziché la duplicazione dell'array) in tutti i casi. Se la matrice viene modificata durante il ciclo foreachname__, a quel punto si verificherà una duplicazione (secondo copy-on-write) e foreachcontinuerà 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: un'iterazione di valore di un array will always 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 a 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 gestita dagli oggetti (cioè 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 sempre a funzionare sugli elementi originali. (In questo caso, anche refcountinge il comportamento di duplicazione sono esattamente gli stessi tra PHP 5 e PHP 7).

  • Cambiamenti del test case 3: Foreachnon utilizza più lo 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() duplicheranno l'array prima di cambiare lo IAP, mentre foreachutilizza ancora l'array originale. (Non che la modifica IAP avrebbe avuto importanza, anche se l'array era condiviso).

La seconda serie di esempi era relativa al comportamento di current() in diverse configurazioni reference/refcounting. Questo non ha più senso, dato che 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 un IAP condiviso.

Un altro caso Edge 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 uguale all'elemento rimosso (a causa di collisione di hash e puntatore). Poiché non ci basiamo più sull'hash dell'elemento per nulla, questo non è più un problema.

1527
NikiC

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 matrici 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 immaginare che 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 pubblicati 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: The PHP Ternary Operator: Fast o not?

108
linepogl

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

a) foreach funziona su prospect copydell'array originale. Significa che foreach() avrà lo SHARED data storage fino a quando non viene creato un prospected copyforeach Note/commenti utente .

b) Cosa attiva una copia prospettata? Una copia prospettica viene creata in base alla politica di copy-on-write, ovvero, ogni volta che viene modificato un array passato a foreach(), viene creato un clone dell'array originale.

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

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

L'esempio seguente mostra che each () e reset () NON influenzano le variabili SENTINEL(for example, the current index variable) della 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/>";

Uscita:

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
43
sakhunzai

NOTA PER PHP 7

Per aggiornare questa risposta in quanto ha acquisito una certa popolarità: questa risposta non si applica più 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. Ad ogni iterazione, il valore dell'elemento corrente viene assegnato a $ value e il puntatore dell'array interno viene avanzato di uno (quindi nella prossima iterazione, si guarderà al 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 esso è 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 su ogni parte di iterazione della spiegazione nella documentazione, il che probabilmente significa che foreach fa tutta la logica prima di chiamare 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 foreach era già nell'ultimo elemento dell'array, ha "deciso" di non eseguire più il ciclo, e anche se abbiamo aggiunto un nuovo elemento, lo abbiamo aggiunto "troppo tardi" e non era collegato.

Una spiegazione dettagliata può essere letta in Come funziona effettivamente PHP 'foreach'? che spiega gli interni di questo comportamento.

29
Damir Kasipovic

Secondo la documentazione fornita da PHP manual.

Ad ogni iterazione, il valore dell'elemento corrente viene assegnato a $ v e l'interno
Il puntatore dell'array è avanzato di uno (quindi alla 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 ha solo un singolo elemento, così come per l'esecuzione di foreach, 1 assegna a $v e 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 ha due elementi, quindi ora $ array valuta gli indici zero e sposta il puntatore di uno. Per la prima iterazione del ciclo, aggiunto $array['baz']=3; come passaggio per riferimento.

14
user3535130

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 semplice ciclo foreach. 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 alla fine:

$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 anche 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 utilizzato per il loopover, ma quando $ set è stato utilizzato all'interno del ciclo, PHP ha aggiunto le variabili all'array originale, non l'array copiato. Fondamentalmente, PHP sta usando 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, e se avessimo 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 abbia esaurito la memoria molto rapidamente, ti suggerisco di fare lo stesso se stai eseguendo 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 per tutta l'esecuzione del ciclo, quindi non avrai mai problemi.

11
hrvojeA

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 dell'array piuttosto che su quello 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 permette di usare anche iterated values as a reference to the original array value. 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 di essere usato come references.

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

7
Pranav Rana