Come funziona realmente il "foreach" di PHP?


2019

Lasciami aggiungere questo prefisso dicendo che so cosa foreachè, cosa fa e come usarlo. Questa domanda riguarda il modo in cui funziona sotto il cofano, e non voglio risposte sulla falsariga di "questo è il modo in cui esegui il ciclo di un array foreach".


Per molto tempo ho pensato che foreachfunzionasse 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 recentemente sono entrato in una discussione sull'argomento e, dopo un po 'di sperimentazione, ho scoperto che questo non era in realtà vero al 100%.

Lasciami mostrare cosa intendo. Per i seguenti casi di test, lavoreremo con il seguente array:

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

Caso di prova 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 */

Ciò dimostra chiaramente che non stiamo lavorando direttamente con l'array di origine, altrimenti il ​​loop continuerebbe per sempre, poiché spingiamo costantemente gli elementi sull'array durante il loop. Ma per essere sicuro questo è il caso:

Caso di prova 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 */

Questo supporta la nostra conclusione iniziale, stiamo lavorando con una copia dell'array di origine durante il ciclo, altrimenti vedremmo i valori modificati durante il ciclo. Ma...

Se guardiamo nel manuale , troviamo questa affermazione:

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

Bene ... questo sembra suggerire che foreachsi basa sul puntatore dell'array dell'array di origine. Ma abbiamo appena dimostrato che non stiamo lavorando con l'array di origine , giusto? Bene, non del tutto.

Caso di prova 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 il fatto che non stiamo lavorando direttamente con l'array di origine, stiamo lavorando direttamente con il puntatore dell'array di origine - il fatto che il puntatore si trova alla fine dell'array alla fine del ciclo lo dimostra. Tranne questo non può essere vero - se lo fosse, allora il caso test 1 andrebbe in loop per sempre.

Il manuale di PHP afferma inoltre:

Poiché foreach si basa sul puntatore di array interno, la modifica all'interno del loop può comportare comportamenti imprevisti.

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

Caso di prova 4 :

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

/* Output: 1 2 3 4 5 */

Caso di prova 5 :

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

/* Output: 1 2 3 4 5 */

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


La domanda

Cosa sta succedendo qui? Il mio C-fu non è abbastanza buono per me da riuscire a trarre una conclusione corretta semplicemente guardando il codice sorgente di PHP, lo apprezzerei se qualcuno potesse tradurlo in inglese per me.

Mi sembra che foreachfunzioni con una copia dell'array, ma imposta il puntatore dell'array dell'array di origine alla fine dell'array dopo il ciclo.

  • È corretto e l'intera storia?
  • In caso contrario, cosa sta facendo davvero?
  • Esiste una situazione in cui l'utilizzo di funzioni che regolano il puntatore di array ( each(), reset()et al.) Durante a foreachpotrebbe influenzare il risultato del loop?

5
@DaveRandom C'è un tag php-internals con cui probabilmente dovrebbe andare, ma lascio a te decidere quale degli altri 5 tag sostituire.
Michael Berkowski,

5
sembra COW, senza maniglia di cancellazione
zb '

149
All'inizio ho pensato »Accidenti, un'altra domanda da principiante. Leggi i documenti ... hm, comportamento chiaramente indefinito «. Poi ho letto la domanda completa e devo dire: mi piace. Ci hai messo un po 'di sforzo e hai scritto tutte le prove. ps. i testcase 4 e 5 sono uguali?
Knittl

21
Solo un pensiero sul perché ha senso che il puntatore di array venga toccato: PHP deve reimpostare e spostare il puntatore di array interno dell'array originale insieme alla copia, poiché l'utente può chiedere un riferimento al valore corrente ( foreach ($array as &$value)) - PHP deve conoscere la posizione corrente nell'array originale anche se in realtà sta iterando su una copia.
Niko

4
@Sean: IMHO, la documentazione di PHP è davvero pessima nel descrivere le sfumature delle funzionalità del linguaggio principale. Ma questo è forse perché molti casi speciali ad hoc sono cotti nella lingua ...
Oliver Charlesworth,

Risposte:


1660

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

Di seguito, cercherò di spiegare con precisione come funziona l'iterazione in diversi casi. Di gran lunga il caso più semplice sono gli Traversableoggetti, in quanto per questi foreachè essenzialmente solo 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 ai metodi effettivi vengono evitate utilizzando un'API interna che essenzialmente rispecchia semplicemente l' Iteratorinterfaccia a livello C.

L'iterazione di matrici e oggetti semplici è significativamente più complicata. Prima di tutto, si dovrebbe notare che in PHP gli "array" sono dizionari realmente ordinati e saranno attraversati secondo questo ordine (che corrisponde all'ordine di inserimento fintanto che non hai usato qualcosa di simile sort). Ciò si oppone all'iterazione dell'ordine naturale delle chiavi (come funzionano spesso 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 una gestione della visibilità. Nella maggior parte dei casi, le proprietà dell'oggetto non vengono effettivamente archiviate in questo modo piuttosto inefficiente. Tuttavia, se si inizia a iterare su un oggetto, la rappresentazione compressa normalmente utilizzata verrà convertita in un dizionario reale. A quel punto, l'iterazione di oggetti semplici diventa molto simile all'iterazione di array (motivo per cui non sto discutendo molto dell'iterazione di oggetti semplici qui).

Fin qui tutto bene. Scorrere 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. Esistono diversi modi in cui ciò può accadere:

  • Se si scorre con riferimento utilizzando foreach ($arr as &$v)poi $arrviene trasformato in un punto di riferimento e si può cambiare durante l'iterazione.
  • In PHP 5 lo stesso vale anche se si esegue l'iterazione per valore, ma l'array era un riferimento in precedenza: $ref =& $arr; foreach ($ref as $v)
  • Gli oggetti hanno una semantica di passaggio by-handle, che per scopi pratici significa che si comportano come riferimenti. Quindi gli oggetti possono sempre essere cambiati durante l'iterazione.

Il problema con consentire le modifiche durante l'iterazione è il caso in cui l'elemento in cui ci si trova attualmente viene rimosso. Supponi di utilizzare un puntatore per tenere traccia di quale elemento dell'array ti trovi attualmente. Se questo elemento viene ora liberato, ti rimane un puntatore penzolante (che di solito si traduce in un segfault).

Esistono diversi modi per risolvere questo problema. PHP 5 e PHP 7 differiscono significativamente in questo senso e descriverò entrambi i comportamenti di seguito. Il riassunto è che l'approccio di PHP 5 era piuttosto stupido e portava a tutti i tipi di strani casi limite, mentre l'approccio più coinvolto di PHP 7 porta a comportamenti più prevedibili e coerenti.

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 "copi" un valore, in realtà riutilizzi il vecchio valore e ne aumenti il ​​conteggio di riferimento (refcount). Solo una volta effettuata una qualche modifica, verrà eseguita una copia reale (chiamata "duplicazione"). Vedi Ti viene mentito per un'introduzione più ampia su questo argomento.

PHP 5

Puntatore di array interno e HashPointer

Gli array in PHP 5 hanno un "puntatore a array interno" (IAP) dedicato, che supporta correttamente le modifiche: ogni volta che un elemento viene rimosso, verrà verificato se lo IAP punta a questo elemento. In tal caso, viene invece spostato all'elemento successivo.

Mentre foreachfa uso di IAP, c'è un'ulteriore complicazione: esiste solo un IAP, ma un array può far parte di più foreachloop:

// 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 ad array interno, foreachesegui i seguenti shenanigans: Prima che il corpo del loop venga eseguito, foreacheseguirà il backup di un puntatore sull'elemento corrente e sul suo hash in una for-foreach HashPointer. Dopo l'esecuzione del corpo del loop, l'IAP verrà ripristinato su questo elemento se esiste ancora. Se tuttavia l'elemento è stato rimosso, utilizzeremo semplicemente ovunque si trovi lo IAP. Questo schema è per lo più una specie di lavoro, ma ci sono molti comportamenti strani che puoi ottenere, alcuni dei quali mostrerò di seguito.

Duplicazione di array

L'IAP è una caratteristica visibile di un array (esposto attraverso la currentfamiglia di funzioni), in quanto tali modifiche al conteggio degli IAP sono modificazioni nella semantica della copia su scrittura. Questo, sfortunatamente, significa che foreachin molti casi è costretto a duplicare l'array su cui sta ripetendo. Le condizioni precise sono:

  1. L'array non è un riferimento (is_ref = 0). Se è un riferimento, si suppone che le modifiche si propaghino, quindi non dovrebbe essere duplicato.
  2. L'array ha refcount> 1. Se refcountè 1, l'array non è condiviso e siamo liberi di modificarlo direttamente.

Se l'array non è duplicato (is_ref = 0, refcount = 1), refcountverrà incrementato solo il suo (*). Inoltre, se foreachper riferimento viene utilizzato, l'array (potenzialmente duplicato) verrà trasformato in un 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, $arrsaranno duplicati per evitare che le modifiche IAP su $arrperdano $outerArr. In termini delle condizioni precedenti, l'array non è un riferimento (is_ref = 0) e viene utilizzato in due posizioni (refcount = 2). Questo requisito è sfortunato e un artefatto dell'implementazione non ottimale (non vi è alcun problema di modifica durante l'iterazione qui, quindi non abbiamo davvero bisogno di usare lo IAP in primo luogo).

(*) L'aumento del refcountqui sembra innocuo, ma viola la semantica della copia su scrittura (COW): ciò significa che modificheremo lo IAP di un array refcount = 2, mentre COW impone che le modifiche possano essere eseguite solo su refcount = 1 valori Questa violazione comporta una modifica del comportamento visibile dall'utente (mentre una 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) duplicare sempre, b) non incrementare refcounte quindi consentire l'array iterato di essere arbitrariamente modificato nel ciclo ec) non usare affatto lo IAP (il PHP 7 soluzione).

Ordine avanzamento posizione

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

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

Tuttavia 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à spostato in avanti prima dell'esecuzione del corpo del loop. Ciò significa che mentre il corpo del loop sta lavorando su element $i, lo IAP è già su element $i+1. Questo è il motivo per cui gli esempi di codice che mostrano modifiche durante l'iterazione saranno sempre unsetl' elemento successivo , anziché quello corrente.

Esempi: i tuoi casi di test

I tre aspetti sopra descritti dovrebbero fornire un'impressione per lo più completa delle idiosincrasie foreachdell'implementazione e 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 $arrayinizia con refcount = 1, quindi non sarà duplicato da foreach: refcountViene incrementato solo il valore . Quando il corpo del loop successivamente modifica l'array (che ha refcount = 2 in quel punto), la duplicazione avverrà 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 viene duplicato, pertanto foreachverrà modificato lo IAP della $arrayvariabile. Alla fine dell'iterazione, lo IAP è NULL (che significa che l'iterazione è stata eseguita), che eachindica restituendo false.

  • Nei casi di test 4 e 5 entrambi eache resetsono funzioni di riferimento. L' $arrayha una refcount=2quando è passata a loro, in modo che deve essere duplicato. Come tale foreachfunzionerà di nuovo su un array separato.

Esempi: effetti di currentin foreach

Un buon modo per mostrare i vari comportamenti di duplicazione è osservare il comportamento della current()funzione all'interno di un foreachloop. 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 per giocare bene con tutte le altre funzioni come quelle nextche sono tutte by-ref. Il passaggio per riferimento implica che l'array deve essere separato e quindi $arraye foreach-arraysarà diverso. Il motivo che si ottiene al 2posto di 1è anche menzionato sopra: fa foreachavanzare il puntatore dell'array prima di eseguire il codice utente, non dopo. Quindi, anche se il codice è al primo elemento, è foreachgià 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 (proprio come sopra). Ma ora che è un riferimento, l'array non deve più essere duplicato quando si passa alla current()funzione by-ref . Quindi current()e foreachlavora sullo stesso array. Si vede comunque il comportamento off-by-one, 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 farà $arrayun is_ref = 1 quando viene iterato per riferimento, quindi sostanzialmente hai la stessa situazione di cui sopra.

Un'altra piccola variazione, questa volta assegneremo l'array a un'altra variabile:

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

Qui il refcount di $arrayè 2 all'avvio del loop, quindi per una volta dobbiamo effettivamente fare la duplicazione in anticipo. Pertanto, $arrayl'array utilizzato da foreach sarà completamente separato dall'inizio. Ecco perché ottieni la posizione dell'IAP dovunque fosse prima del loop (in questo caso era nella prima posizione).

Esempi: modifica durante l'iterazione

Cercare di rendere conto delle modifiche durante l'iterazione è dove sono nati tutti i nostri problemi di foreach, quindi serve prendere in considerazione alcuni esempi per questo caso.

Considera questi loop nidificati sullo stesso array (dove viene usata l'iterazione by-ref per assicurarsi che sia davvero lo stesso):

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 dall'output perché l'elemento è 1stato 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 nidificato sopra descritto: prima che il corpo del loop venga eseguito, viene eseguito il backup della posizione IAP corrente e dell'hash in a HashPointer. Dopo il corpo del loop verrà ripristinato, ma solo se l'elemento esiste ancora, altrimenti verrà utilizzata la posizione IAP corrente (qualunque essa sia). Nell'esempio sopra questo è esattamente il caso: l'elemento corrente del loop esterno è stato rimosso, quindi utilizzerà lo IAP, che è già stato contrassegnato come completato dal loop interno!

Un'altra conseguenza del HashPointermeccanismo di backup + ripristino è che le modifiche allo IAP attraverso reset()ecc. Di solito non incidono foreach. Ad esempio, il codice seguente viene eseguito come se reset()non fossero 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 sull'elemento foreach corrente dopo il corpo del loop. Per forzare reset()l'effetto sul loop, è necessario 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 di mente. Il vero divertimento inizia se ricordi che il HashPointerripristino utilizza un puntatore all'elemento e al suo hash per determinare se esiste ancora. Ma: gli hash hanno delle collisioni e i puntatori possono essere riutilizzati! Ciò significa che, con un'attenta scelta delle chiavi dell'array, possiamo far foreachcredere che esiste ancora un elemento che è stato rimosso, quindi passerà 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, 4secondo le regole precedenti. Ciò che accade è che 'FYFY'ha lo stesso hash dell'elemento rimosso 'EzFY'e l'allocatore riesce a riutilizzare la stessa posizione di memoria per memorizzare l'elemento. Quindi foreach finisce direttamente saltando sull'elemento appena inserito, interrompendo così il ciclo.

Sostituendo l'entità iterata durante il ciclo

Un ultimo caso strano che vorrei menzionare, è che PHP ti consente di sostituire l'entità iterata durante il ciclo. Quindi puoi iniziare iterando su un array e poi sostituirlo con un altro array a metà. Oppure avvia iterando su un array e poi sostituiscilo 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à a iterare l'altra entità dall'inizio una volta avvenuta la sostituzione.

PHP 7

Iteratori hashtable

Se ricordi ancora, il problema principale con l'iterazione dell'array era come gestire la rimozione degli elementi durante l'iterazione. PHP 5 ha utilizzato un singolo puntatore a matrice interna (IAP) per questo scopo, che era in qualche modo non ottimale, poiché un puntatore a matrice doveva essere allungato per supportare più cicli foreach simultanei e interazione con reset()ecc.

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 quel momento in poi hanno la stessa semantica dell'IAP: se un elemento dell'array viene rimosso, tutti gli iteratori hashtable che puntano a quell'elemento verranno portati all'elemento successivo.

Ciò significa che foreachnon sarà più utilizzare il PAI affatto . Il foreachloop non avrà assolutamente alcun effetto sui risultati di current()ecc. E il suo comportamento non sarà mai influenzato da funzioni come reset()ecc.

Duplicazione di array

Un altro cambiamento importante tra PHP 5 e PHP 7 riguarda la duplicazione di array. Ora che lo IAP non viene più utilizzato, l'iterazione di array in base al valore farà solo un refcountincremento (anziché duplicare l'array) in tutti i casi. Se l'array viene modificato durante il foreachciclo, a quel punto si verificherà una duplicazione (in base alla copia su scrittura) e foreachcontinuerà a funzionare sull'array precedente.

Nella maggior parte dei casi, questa modifica è trasparente e non ha altri effetti che prestazioni migliori. Tuttavia, vi è un'occasione in cui risulta un comportamento diverso, vale a dire il caso in cui l'array era un riferimento in anticipo:

$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 passato, l'iterazione in valore per array di riferimenti era casi speciali. In questo caso, non si è verificata alcuna duplicazione, quindi tutte le modifiche dell'array durante l'iterazione sarebbero riflesse dal loop. In PHP 7 questo caso speciale è sparito: un'iterazione per valore di un array continuerà sempre a funzionare sugli elementi originali, ignorando qualsiasi modifica durante il ciclo.

Questo, ovviamente, non si applica all'iterazione per riferimento. Se si itera per riferimento, tutte le modifiche verranno riflesse dal loop. È interessante notare che lo stesso vale per l'iterazione per 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 by-handle degli oggetti (cioè si comportano come riferimenti anche in contesti per valore).

Esempi

Consideriamo alcuni esempi, a partire dai casi di test:

  • I casi di test 1 e 2 mantengono lo stesso output: l'iterazione dell'array per valore continua a funzionare sempre sugli elementi originali. (In questo caso, il refcountingcomportamento pari e duplicato è esattamente lo stesso tra PHP 5 e PHP 7).

  • Test case 3 cambia: Foreachnon utilizza più lo IAP, quindi each()non è influenzato dal loop. Avrà lo stesso output prima e dopo.

  • I casi di test 4 e 5 rimangono gli stessi: each()e reset()duplicheranno l'array prima di modificare lo IAP, pur foreachusando l'array originale. (Non che la modifica IAP avrebbe avuto importanza, anche se l'array fosse condiviso.)

La seconda serie di esempi era correlata al comportamento di current()diverse reference/refcountingconfigurazioni. Questo non ha più senso, poiché non current()è completamente influenzato dal loop, quindi il suo valore restituito rimane sempre lo stesso.

Tuttavia, otteniamo alcune modifiche 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 hashtable completamente separati e non esiste più alcuna contaminazione incrociata di entrambi i loop attraverso un IAP condiviso.

Un altro strano caso limite che è stato risolto ora, è lo strano effetto 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 passava direttamente al nuovo elemento perché "sembrava" uguale all'elemento rimosso (a causa di hash e puntatore in conflitto). Dato che non facciamo più affidamento sull'hash dell'elemento per nulla, questo non è più un problema.


4
@Baba Lo fa. Passarlo a una funzione è lo stesso che fare $foo = $arrayprima del ciclo;)
NikiC,

32
Per quelli di voi che non sanno cosa sia uno zval, si prega di fare riferimento al blog
shu zOMG chen,

1
Correzione minore: ciò che chiami Bucket non è ciò che normalmente viene chiamato Bucket in una tabella hash. Normalmente Bucket è un insieme di voci con la stessa dimensione hash%. Sembra che tu lo usi per quella che normalmente viene chiamata voce. L'elenco collegato non è sui bucket, ma sulle voci.
unbeli

12
@unbeli Sto usando la terminologia utilizzata internamente da PHP. Gli Buckets fanno parte di un elenco doppiamente collegato per le collisioni di hash e fanno anche parte di un elenco doppiamente collegato per ordine;)
NikiC

4
Grande risposta. Penso che intendevi iterate($outerArr);e non iterate($arr);da qualche parte.
niahoo,

116

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

L'operatore di assegnazione per gli array in PHP funziona più come un clone pigro. L'assegnazione di una variabile a un'altra che contiene un array clonerà l'array, 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 avrà luogo solo quando viene modificata una delle variabili (copia su scrittura).

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 casi di test, puoi facilmente immaginare che foreachcrei una sorta di iteratore con un riferimento all'array. Questo riferimento funziona esattamente come la variabile $bnel mio esempio. Tuttavia, l'iteratore insieme al riferimento vive solo durante il ciclo e quindi vengono entrambi scartati. Ora puoi vedere che, in tutti i casi tranne 3, l'array viene modificato durante il ciclo, mentre questo riferimento extra è attivo. Questo innesca un clone e spiega cosa sta succedendo qui!

Ecco un eccellente articolo per un altro effetto collaterale di questo comportamento di copia su scrittura: L'operatore ternario PHP: veloce o no?


sembra che tu abbia ragione, ho fatto un esempio che dimostra che: codepad.org/OCjtvu8r una differenza dal tuo esempio: non viene copiato se si cambia valore, solo se si cambiano le chiavi.
zb "

Questo in effetti spiega tutto il comportamento sopra riportato, e può essere ben illustrato chiamando each()alla fine del primo caso di test, in cui vediamo che il puntatore dell'array dell'array originale punta al secondo elemento, poiché l'array è stato modificato durante la prima iterazione. Questo sembra anche dimostrare che foreachsposta il puntatore dell'array prima di eseguire il blocco di codice del loop, cosa che non mi aspettavo, avrei pensato che alla fine sarebbe stato così. Molte grazie, questo mi chiarisce bene.
DaveRandom,

49

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

a) foreachlavora sulla copia prospettata dell'array originale. Significa foreach()che avrà l'archiviazione dei dati CONDIVISI fino a quando non prospected copyviene creato un per ogni commento Note / Utente .

b) Cosa attiva una copia presunta ? Una copia prospettata viene creata in base alla politica di copy-on-write, ovvero ogni volta che foreach()viene modificata una matrice, viene creato un clone della matrice originale.

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

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

L'esempio seguente mostra che ogni () e reset () non influenza SENTINELle variabili (for example, the current index variable)del foreach()iteratore.

$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

2
La tua risposta non è del tutto corretta. foreachopera su una potenziale copia dell'array, ma non esegue la copia effettiva a meno che non sia necessaria.
linepogl

ti piacerebbe dimostrare come e quando quella potenziale copia viene creata attraverso il codice? Il mio codice dimostra che foreachsta copiando l'array il 100% delle volte. Sono ansioso di sapere. Grazie per i tuoi commenti
sakhunzai,

La copia di un array costa molto. Prova a contare il tempo impiegato per iterare un array con 100000 elementi usando foro foreach. Non vedrai alcuna differenza significativa tra i due, perché una copia effettiva non ha luogo.
linepogl

Quindi suppongo che ci sia SHARED data storageriservato fino o meno copy-on-write, ma (dal mio frammento di codice) è evidente che ci sarà sempre DUE set di SENTINEL variablesuno per l' original arrayaltro e per foreach. Grazie che ha un senso
sakhunzai

1
sì, è una copia "prospettata" ovvero una copia "potenziale". Non è protetta come da lei suggerito
sakhunzai

33

NOTA PER PHP 7

Per aggiornare su questa risposta in quanto ha guadagnato un po 'di popolarità: questa risposta non si applica più a partire da PHP 7. Come spiegato in " Modifiche incompatibili all'indietro ", in PHP 7 foreach funziona su 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 fornito da array_expression. Ad ogni iterazione, il valore dell'elemento corrente è assegnato a $ value e il puntatore di array interno è avanzato di uno (quindi nella iterazione successiva, vedrai l'elemento successivo).

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 un nuovo elemento foreach termina perché ha già "deciso" che esso è l'ultimo elemento.

Nel tuo secondo esempio, inizi con due elementi e il ciclo foreach non è all'ultimo elemento, quindi valuta l'array nella successiva iterazione e si rende quindi conto che ci sono nuovi elementi nell'array.

Credo che questa sia tutta conseguenza di ogni parte dell'iterazione della spiegazione nella documentazione, il che probabilmente significa che foreachfa tutta la logica prima di chiamare il codice {}.

Caso di prova

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
)

Ciò significa che ha accettato la modifica e l'ha attraversata 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
)

Ciò significa che l'array è stato modificato, ma dal momento che l'abbiamo modificato quando foreachera già all'ultimo elemento dell'array, "ha deciso" di non eseguire più il loop e anche se abbiamo aggiunto un nuovo elemento, l'abbiamo aggiunto "troppo tardi" e non è stato ripetuto.

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


7
Bene hai letto il resto della risposta? Ha perfettamente senso che foreach decida se eseguirà il loop un'altra volta prima ancora che esegua il codice al suo interno.
dkasipovic,

2
No, l'array viene modificato, ma "troppo tardi" poiché foreach già "pensa" che si trova all'ultimo elemento (che è all'inizio dell'iterazione) e non eseguirà più il loop. Dove nel secondo esempio, non si trova nell'ultimo elemento all'inizio dell'iterazione e valuta nuovamente all'inizio dell'iterazione successiva. Sto cercando di preparare un caso di prova.
dkasipovic,

1
@AlmaDo Guarda lxr.php.net/xref/PHP_TRUNK/Zend/zend_vm_def.h#4509 È sempre impostato sul puntatore successivo quando viene ripetuto. Quindi, quando raggiunge l'ultima iterazione, verrà contrassegnato come finito (tramite puntatore NULL). Quando poi aggiungi una chiave nell'ultima iterazione, foreach non la noterà.
bwoebi,

1
@DKasipovic no. Non c'è una spiegazione completa e chiara lì (almeno per ora - forse sbaglio)
Alma Do

4
In realtà sembra che @AlmaDo abbia un difetto nel comprendere la propria logica ... La tua risposta va bene.
bwoebi,

15

Come da documentazione fornita dal manuale di PHP.

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

Come per il tuo primo esempio:

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

$arrayhanno solo un singolo elemento, quindi come per l'esecuzione foreach, 1 assegna 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);
}

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


13

Ottima domanda, perché molti sviluppatori, anche esperti, sono confusi dal modo in cui PHP gestisce le matrici nei cicli foreach. Nel ciclo foreach standard, PHP crea una copia dell'array che viene utilizzato nel ciclo. La copia viene eliminata immediatamente al termine del ciclo. Ciò è 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, perché l'array originale non fa riferimento all'interno del ciclo o al termine del ciclo. 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 rispetto all'originale non possono essere notifiche, 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 stai lavorando. Puoi sovrascriverlo prendendo $ 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 operato per riferimento, le modifiche apportate a $ item vengono fatte ai membri del $ set originale. L'uso di $ item per riferimento impedisce anche a PHP di creare la copia dell'array. Per provare questo, per prima cosa 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 loop over, ma quando $ set è stato usato all'interno del loop, PHP ha aggiunto le variabili all'array originale, non all'array copiato. Fondamentalmente, PHP sta usando l'array copiato solo per l'esecuzione del loop e l'assegnazione di $ item. Per questo motivo, il ciclo sopra 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 non inserendo mai un ciclo infinito.

Tuttavia, se avessimo usato $ item come riferimento, come ho già detto prima? Un singolo carattere aggiunto al test sopra:

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

Risultati in un ciclo infinito. Nota che questo è in realtà un ciclo infinito, dovrai uccidere tu stesso lo script o attendere che il tuo sistema operativo esaurisca la memoria. Ho aggiunto la seguente riga al mio script in modo che PHP esaurisca la memoria molto rapidamente, ti suggerisco di fare lo stesso se eseguirai questi test loop infiniti:

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

Quindi in questo esempio precedente 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 del ciclo stesso, l'array rimane statico durante l'esecuzione del ciclo, quindi non si verificheranno mai problemi.


7

PHP foreach loop può essere usato con Indexed arrays, Associative arrayse Object public variables.

Nel ciclo foreach, la prima cosa che fa php è che crea una copia dell'array su cui deve essere ripetuto. PHP esegue quindi l'iterazione su questo nuovo copyarray 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 ciò, php consente anche di usare 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 indexesl'utilizzo come references.

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


1
Object public variablesè sbagliato o nella migliore delle ipotesi fuorviante. Non è possibile utilizzare un oggetto in un array senza l'interfaccia corretta (ad es. Traversible) e quando lo si fa foreach((array)$obj ...si lavora con un array semplice, non più un oggetto.
Christian,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.