web-dev-qa-db-fra.com

Comment fonctionne PHP 'foreach'?

Permettez-moi de préfixer ceci en disant que je sais ce que foreach est, fait et comment l’utiliser. Cette question concerne le fonctionnement de celui-ci sous le capot, et je ne souhaite pas de réponse du type "voici comment vous mettez en boucle un tableau avec foreach".


Pendant longtemps, j'ai supposé que foreach fonctionnait avec le tableau lui-même. Ensuite, j'ai trouvé de nombreuses références au fait que cela fonctionne avec un copie du tableau, et j'ai depuis supposé que c'était la fin de l'histoire. Mais récemment, j'ai entamé une discussion sur le sujet et, après un peu d'expérimentation, j'ai constaté que ce n'était pas vrai à 100%.

Laissez-moi montrer ce que je veux dire. Pour les cas de test suivants, nous allons utiliser le tableau suivant:

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

Cas de test 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 */

Cela montre clairement que nous ne travaillons pas directement avec le tableau source. Dans le cas contraire, la boucle continuerait indéfiniment, car nous ajoutons constamment des éléments au tableau pendant la boucle. Mais juste pour être sûr que c'est le cas:

Cas de test 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 */

Ceci confirme notre conclusion initiale, nous travaillons avec une copie du tableau source pendant la boucle, sinon nous verrions les valeurs modifiées pendant la boucle. Mais ...

Si nous regardons dans le manuel , nous trouvons cette déclaration:

Lorsque foreach commence sa première exécution, le pointeur de tableau interne est automatiquement réinitialisé sur le premier élément du tableau.

Bon ... cela semble suggérer que foreach s'appuie sur le pointeur de tableau du tableau source. Mais nous venons de prouver que nous sommes ne travaillons pas avec le tableau source, n'est-ce pas? Eh bien, pas tout à fait.

Cas de test :

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

Ainsi, malgré le fait que nous ne travaillons pas directement avec le tableau source, nous travaillons directement avec le pointeur de tableau source - le fait que le pointeur se trouve à la fin du tableau à la fin de la boucle montre cela. Sauf que cela ne peut pas être vrai - si c'était le cas, alors scénario de test 1 resterait en boucle pour toujours.

Le manuel PHP indique également:

Comme foreach repose sur le pointeur de tableau interne, sa modification dans la boucle peut entraîner un comportement inattendu.

Eh bien, voyons ce qu'est ce "comportement inattendu" (techniquement, tout comportement est inattendu puisque je ne sais plus à quoi m'attendre).

Cas de test 4 :

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

/* Output: 1 2 3 4 5 */

Cas de test 5 :

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

/* Output: 1 2 3 4 5 */

... rien d’inattendu là-bas, en fait, il semble appuyer la théorie de la "copie de source".


La question

Qu'est-ce qui se passe ici? Mon C-fu n'est pas assez bon pour que je puisse extraire une conclusion correcte simplement en regardant le code source PHP, je l'apprécierais si quelqu'un pouvait le traduire en anglais pour moi.

Il me semble que foreach fonctionne avec une copie du tableau, mais définit le pointeur de tableau du tableau source à la fin du tableau après la boucle.

  • Est-ce correct et toute l'histoire?
  • Si non, que fait-il vraiment?
  • Existe-t-il une situation dans laquelle l'utilisation de fonctions qui ajustent le pointeur de tableau (each(), reset() et autres) pendant une foreach pourrait affecter le résultat de la boucle?
1870
DaveRandom

foreach prend en charge l'itération sur trois types de valeurs différents:

Dans la suite, j'essaierai d'expliquer précisément le fonctionnement de l'itération dans différents cas. Le cas de loin le plus simple est celui des objets Traversable, car pour ceux-ci, foreach est essentiellement un sucre de syntaxe pour le code dans les termes suivants:

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

Pour les classes internes, les appels de méthode réels sont évités en utilisant une API interne qui reflète essentiellement l'interface Iterator au niveau C.

L'itération de tableaux et d'objets simples est considérablement plus compliquée. Tout d’abord, il convient de noter que, dans PHP, "les tableaux" sont des dictionnaires vraiment ordonnés et qu’ils seront parcourus dans cet ordre (qui correspond à l’ordre d’insertion tant que vous n’avez pas utilisé sort ). Cela s'oppose à une itération par l'ordre naturel des clés (comment fonctionnent souvent les listes dans d'autres langues) ou à l'absence d'un ordre défini (comment fonctionnent souvent les dictionnaires dans d'autres langues).

Il en va de même pour les objets, car les propriétés de l'objet peuvent être considérées comme un autre nom (ordonné) de mappage des noms de propriété du dictionnaire avec leurs valeurs, plus un traitement de visibilité. Dans la majorité des cas, les propriétés de l'objet ne sont pas réellement stockées de cette manière plutôt inefficace. Toutefois, si vous commencez à itérer sur un objet, la représentation condensée normalement utilisée sera convertie en dictionnaire réel. À ce stade, l'itération des objets simples devient très similaire à l'itération des tableaux (c'est pourquoi je ne parle pas beaucoup ici de l'itération des objets simples).

Jusqu'ici tout va bien. Itérer sur un dictionnaire ne peut pas être trop difficile, non? Les problèmes commencent lorsque vous réalisez qu'un tableau/objet peut changer pendant l'itération. Cela peut se produire de plusieurs manières:

  • Si vous effectuez une itération par référence à l'aide de foreach ($arr as &$v), alors $arr est transformé en une référence que vous pouvez modifier au cours de l'itération.
  • Dans PHP 5, la même chose s'applique même si vous effectuez une itération par valeur, mais le tableau était auparavant une référence: $ref =& $arr; foreach ($ref as $v)
  • La sémantique des objets est transmise par les objets, ce qui signifie qu’ils se comportent comme des références. Ainsi, les objets peuvent toujours être modifiés lors de l'itération.

Le problème avec l'autorisation de modifications lors de l'itération est le cas où l'élément sur lequel vous êtes actuellement est supprimé. Supposons que vous utilisiez un pointeur pour garder trace de l'élément de tableau dans lequel vous vous trouvez. Si cet élément est maintenant libéré, vous vous retrouvez avec un pointeur suspendu (résultant généralement en une erreur de segmentation).

Il existe différentes façons de résoudre ce problème. PHP 5 et PHP 7 diffèrent considérablement à cet égard et je décrirai les deux comportements ci-après. Le résumé est que l'approche de PHP 5 était plutôt stupide et a entraîné toutes sortes de problèmes étranges concernant les cas Edge, alors que l'approche plus impliquée de PHP 7 a pour résultat un comportement plus prévisible et cohérent.

En guise de dernier préliminaire, il convient de noter que PHP utilise le comptage de références et la copie sur écriture pour gérer la mémoire. Cela signifie que si vous "copiez" une valeur, vous réutilisez simplement l'ancienne valeur et incrémentez son nombre de références (refcount). Une fois que vous avez effectué une sorte de modification, une copie réelle (appelée "duplication") sera créée. Voir On vous ment pour une introduction plus détaillée sur ce sujet.

PHP 5

Pointeur interne et HashPointer

Les tableaux de PHP 5 ont un "pointeur de tableau interne" (IAP) dédié, qui prend correctement en charge les modifications: chaque fois qu'un élément est supprimé, il sera vérifié si le point IAP pointe vers cet élément. Si c'est le cas, il est avancé à l'élément suivant.

Bien que foreach utilise le PAI, il existe une complication supplémentaire: il n'y a qu'un seul PAI, mais un tableau peut faire partie de plusieurs boucles 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) {
        // ...
    }
}

Pour prendre en charge deux boucles simultanées avec un seul pointeur de tableau interne, foreach exécute les manœuvres suivantes: Avant que le corps de la boucle ne soit exécuté, foreach sauvegarde un pointeur sur l'élément actuel et son hachage dans un HashPointer à la source. Une fois le corps de la boucle exécuté, l'IAP sera réinitialisé sur cet élément s'il existe toujours. Si toutefois l'élément a été supprimé, nous ne ferons que l'utiliser là où se trouve actuellement le PAI. Ce schéma fonctionne pour la plupart, en quelque sorte, mais il y a beaucoup de comportements étranges que vous pouvez obtenir, dont certains que je vais montrer ci-dessous.

Duplication de tableaux

L'IAP est une caractéristique visible d'un tableau (exposée par le biais de la famille de fonctions current), de sorte que les modifications apportées à l'IAP sont considérées comme des modifications sous la sémantique de la copie sur écriture. Malheureusement, cela signifie que foreach est souvent obligé de dupliquer le tableau sur lequel il itère. Les conditions précises sont:

  1. Le tableau n'est pas une référence (is_ref = 0). Si c'est une référence, alors les modifications qui y sont apportées sont supposées se propager, elles ne doivent donc pas être dupliquées.
  2. Le tableau a refcount> 1. Si refcount est égal à 1, le tableau n'est pas partagé et nous sommes libres de le modifier directement.

Si le tableau n'est pas dupliqué (is_ref = 0, refcount = 1), seul son refcount sera incrémenté (*). De plus, si vous utilisez foreach par référence, le tableau (potentiellement dupliqué) sera transformé en une référence.

Considérez ce code comme un exemple de duplication:

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

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

Ici, $arr sera dupliqué pour empêcher toute modification des modifications d'IAP sur $arr en $outerArr. En termes de conditions ci-dessus, le tableau n'est pas une référence (is_ref = 0) et est utilisé à deux endroits (refcount = 2). Cette exigence est regrettable et constitue un artefact de l'implémentation sous-optimale (il n'y a aucune préoccupation de modification lors de l'itération ici, nous n'avons donc pas vraiment besoin d'utiliser le PAI en premier lieu).

(*) Incrémenter refcount semble inoffensif, mais viole la sémantique de la copie sur écriture (COW): Cela signifie que nous allons modifier l'IAP d'un tableau refcount = 2, alors que COW indique que les modifications ne peuvent être effectuées que sur refcount. = 1 valeurs. Cette violation entraîne un changement de comportement visible pour l'utilisateur (alors qu'une COW est normalement transparente) car le changement d'IAP sur le tableau itéré sera observable, mais uniquement jusqu'à la première modification non-IAP sur le tableau. Au lieu de cela, les trois options "valides" auraient été a) de toujours dupliquer, b) de ne pas incrémenter le refcount et de permettre ainsi au tableau itéré d'être modifié arbitrairement dans la boucle ou c) de ne pas utiliser du tout le PAI (le PHP 7 solution).

Ordre d'avancement de poste

Vous devez connaître un dernier détail d'implémentation pour bien comprendre les exemples de code ci-dessous. La manière "normale" de parcourir en boucle une structure de données ressemblerait à ceci dans le pseudocode:

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

Cependant, foreach, étant un flocon de neige assez particulier, choisit de faire les choses légèrement différemment:

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

A savoir, le pointeur du tableau est déjà avancé before le corps de la boucle est exécuté. Cela signifie que pendant que le corps de la boucle travaille sur l'élément $i, l'IAP est déjà présent sur l'élément $i+1. C'est la raison pour laquelle les échantillons de code montrant une modification lors de l'itération seront toujours unset l'élément next, plutôt que l'élément en cours.

Exemples: vos cas de test

Les trois aspects décrits ci-dessus devraient vous donner une impression généralement complète des particularités de la mise en œuvre foreach et nous pourrons passer à quelques exemples.

Le comportement de vos cas de test est simple à expliquer à ce stade:

  • Dans les scénarios de test 1 et 2, $array commence avec refcount = 1, de sorte qu'il ne sera pas dupliqué par foreach: seul le refcount est incrémenté. Lorsque le corps de la boucle modifie ultérieurement le tableau (qui a refcount = 2 à ce stade), la duplication aura lieu à ce stade. Foreach continuera à travailler sur une copie non modifiée de $array.

  • Dans le cas de test 3, encore une fois, le tableau n'est pas dupliqué. foreach modifiera donc l'IAP de la variable $array. À la fin de l'itération, l'IAP est NULL (ce qui signifie que itération a été effectuée), ce que each indique en renvoyant false.

  • Dans les cas de test 4 et 5, each et reset sont des fonctions par référence. Le $array a un refcount=2 quand il leur est transmis, il doit donc être dupliqué. En tant que tel, foreach travaillera à nouveau sur un tableau séparé.

Exemples: effets de current dans foreach

Un bon moyen de montrer les différents comportements de duplication consiste à observer le comportement de la fonction current() à l'intérieur d'une boucle foreach. Considérons cet exemple:

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

Ici, vous devez savoir que current() est une fonction by-ref (en fait: prefer-ref), même si elle ne modifie pas le tableau. Cela doit être pour pouvoir jouer à Nice avec toutes les autres fonctions comme next qui sont toutes by-ref. Le passage par référence implique que le tableau doit être séparé et ainsi $array et le foreach-array seront différents. La raison pour laquelle vous obtenez 2 au lieu de 1 est également mentionnée ci-dessus: foreach fait avancer le pointeur de tableau avant ​​exécute le code utilisateur, pas après. Ainsi, même si le code est au premier élément, foreach a déjà avancé le pointeur sur le second.

Essayons maintenant une petite modification:

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

Ici nous avons le cas is_ref = 1, donc le tableau n'est pas copié (juste comme ci-dessus). Mais maintenant que c'est une référence, le tableau n'a plus besoin d'être dupliqué lors du passage à la fonction by-ref current(). Ainsi, current() et foreach fonctionnent sur le même tableau. Vous voyez toujours le comportement "off-by-one", en raison de la façon dont foreach fait avancer le pointeur.

Vous obtenez le même comportement lorsque vous effectuez une itération par référence:

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

Ici, l’important est que foreach fera $array un is_ref = 1 lorsqu’il sera itéré par référence, donc vous avez fondamentalement la même situation que ci-dessus.

Une autre petite variation, cette fois nous allons assigner le tableau à une autre variable:

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

Ici, le nombre de références du $array est égal à 2 lorsque la boucle est démarrée, donc pour une fois, nous devons réellement effectuer la duplication à l’avance. Ainsi, $array et le tableau utilisé par foreach seront complètement séparés dès le départ. C'est pourquoi vous obtenez la position de l'IAP n'importe où avant la boucle (dans ce cas, c'était la première position).

Exemples: modification lors de l'itération

Tenter de prendre en compte les modifications apportées lors de l'itération est à l'origine de tous nos problèmes foreach. Il est donc utile de prendre quelques exemples pour ce cas.

Considérez ces boucles imbriquées sur le même tableau (où l'itération par référence est utilisée pour s'assurer que c'est bien le même):

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 partie attendue ici est que (1, 2) est manquant dans la sortie car l'élément 1 a été supprimé. Ce qui est probablement inattendu, c'est que la boucle externe s'arrête après le premier élément. Pourquoi donc?

La raison derrière ceci est le hack de la boucle imbriquée décrite ci-dessus: Avant que le corps de la boucle ne soit exécuté, la position et le hachage IAP actuels sont sauvegardés dans un HashPointer. Après le corps de la boucle, elle sera restaurée, mais uniquement si l'élément existe toujours, sinon la position actuelle de l'IAP (quelle qu'elle soit) est utilisée à la place. Dans l'exemple ci-dessus, c'est exactement le cas: L'élément actuel de la boucle externe a été supprimé. Il utilisera donc l'IAP, qui a déjà été marqué comme terminé par la boucle interne!

Une autre conséquence du mécanisme de sauvegarde + restauration HashPointer est que les modifications apportées à l'IAP bien que reset() etc. n'affectent généralement pas foreach. Par exemple, le code suivant s'exécute comme si le reset() n'était pas présent du tout:

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

La raison en est que, bien que reset() modifie temporairement l'IAP, il sera restauré à l'élément foreach actuel après le corps de la boucle. Pour forcer reset() à appliquer un effet sur la boucle, vous devez également supprimer l'élément actuel, afin que le mécanisme de sauvegarde/restauration échoue:

$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

Mais, ces exemples sont encore sains. Le vrai plaisir commence si vous vous souvenez que la restauration HashPointer utilise un pointeur sur l'élément et son hachage pour déterminer s'il existe toujours. Mais: les hachages ont des collisions et les pointeurs peuvent être réutilisés! Cela signifie que, avec un choix judicieux de clés de tableau, nous pouvons faire croire à foreach qu'un élément qui a été supprimé existe toujours et qu'il y sautera directement. Un exemple:

$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

Ici, nous devrions normalement nous attendre à la sortie 1, 1, 3, 4 conformément aux règles précédentes. Le résultat est que 'FYFY' a le même hachage que l'élément supprimé 'EzFY', et que l'allocateur réutilise le même emplacement mémoire pour stocker l'élément. Donc, foreach finit par sauter directement à l'élément nouvellement inséré, raccourcissant ainsi la boucle.

Substituer l'entité itérée pendant la boucle

Un dernier cas étrange que je voudrais mentionner, c’est que PHP vous permet de substituer l’entité itérée pendant la boucle. Vous pouvez donc commencer à itérer sur un tableau, puis le remplacer par un autre tableau à mi-parcours. Ou commencez à itérer sur un tableau puis remplacez-le par un objet:

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

Comme vous pouvez le constater dans ce cas, PHP commencera à itérer l’autre entité dès le début, une fois la substitution effectuée.

PHP 7

Itérateurs de la table de hachage

Si vous vous en souvenez encore, le principal problème de l'itération de tableau était de savoir comment gérer la suppression d'éléments à mi-itération. PHP 5 utilisait un seul pointeur de tableau interne (IAP) à cette fin, ce qui était quelque peu sous-optimal, puisqu'un pointeur de tableau devait être étiré pour prendre en charge plusieurs boucles foreach simultanées et ​​interaction avec reset() etc. en plus de cela.

PHP 7 utilise une approche différente, à savoir qu'il prend en charge la création d'un nombre arbitraire d'itérateurs de hashtables externes et sûrs. Ces itérateurs doivent être inscrits dans le tableau, à partir de quel point ils ont la même sémantique que l'IAP: Si un élément de tableau est supprimé, tous les itérateurs de hashtable pointant vers cet élément seront avancés au prochain élément.

Cela signifie que foreach n'utilisera plus du tout le PAI . La boucle foreach n'aura absolument aucun effet sur les résultats de current() etc. et son propre comportement ne sera jamais influencé par des fonctions telles que reset() etc.

Duplication de tableaux

Un autre changement important entre PHP 5 et PHP 7 concerne la duplication de tableaux. Maintenant que l'IAP n'est plus utilisé, l'itération de tableau par valeur n'effectuera qu'un incrément refcount (au lieu de dupliquer le tableau) dans tous les cas. Si le tableau est modifié au cours de la boucle foreach, une duplication se produira (selon la copie sur écriture) et foreach continuera à fonctionner sur l'ancien tableau.

Dans la plupart des cas, ce changement est transparent et n'a pas d'autre effet que de meilleures performances. Cependant, il y a une occasion où il en résulte un comportement différent, à savoir le cas où le tableau était auparavant une référence:

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

Auparavant, l'itération par valeur de tableaux de référence constituait un cas spécial. Dans ce cas, aucune duplication ne s'est produite, de sorte que toutes les modifications du tableau lors de l'itération seront reflétées par la boucle. Dans PHP 7, ce cas particulier a disparu: une itération par valeur d'un tableau sera toujours continue de fonctionner sur les éléments d'origine, sans tenir compte des modifications pendant la boucle.

Ceci, bien sûr, ne s'applique pas à l'itération par référence. Si vous effectuez une itération par référence, toutes les modifications seront reflétées par la boucle. Fait intéressant, il en va de même pour l'itération par valeur des objets simples:

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

Cela reflète la sémantique d’objets par descripteur (c’est-à-dire qu’ils se comportent comme une référence, même dans des contextes de valeur secondaire).

Exemples

Considérons quelques exemples, en commençant par vos cas de test:

  • Les cas de test 1 et 2 conservent le même résultat: l'itération de tableau par valeur continue de fonctionner avec les éléments d'origine. (Dans ce cas, même le comportement de refcounting et de la duplication est exactement le même entre PHP 5 et PHP 7).

  • Le cas de test 3 change: Foreach n'utilise plus l'IAP, donc each() n'est pas affecté par la boucle. Il aura la même sortie avant et après.

  • Les cas de test 4 et 5 restent les mêmes: each() et reset() dupliqueront le tableau avant de modifier l'IAP, tandis que foreach utilisera toujours le tableau d'origine. (Ce n'est pas que le changement d'IAP aurait eu de l'importance, même si le tableau était partagé.)

Le deuxième ensemble d’exemples concernait le comportement de current() sous différentes configurations reference/refcounting. Cela n’a plus de sens, étant donné que current() n’a aucune influence sur la boucle et que sa valeur de retour reste toujours la même.

Cependant, nous obtenons des changements intéressants lorsque nous considérons des modifications lors de l'itération. J'espère que vous trouverez le nouveau comportement plus sain. Le premier exemple:

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

Comme vous pouvez le constater, la boucle externe n’abandonne plus après la première itération. La raison en est que les deux boucles ont maintenant des itérateurs de table de hachage entièrement séparés et qu'il n'y a plus de contamination croisée des deux boucles via un IAP partagé.

Un autre cas étrange de Edge qui est corrigé maintenant est l'effet étrange que vous obtenez lorsque vous supprimez et ajoutez des éléments qui ont le même 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

Auparavant, le mécanisme de restauration HashPointer passait directement au nouvel élément car il "ressemblait" à l'identique de l'élément supprimé (en raison d'une collision entre un hachage et un pointeur). Comme nous ne comptons plus sur l'élément hash, le problème ne se pose plus.

1551
NikiC

Dans l'exemple 3, vous ne modifiez pas le tableau. Dans tous les autres exemples, vous modifiez le contenu ou le pointeur de tableau interne. Ceci est important quand il s'agit de PHP tableaux en raison de la sémantique de l'opérateur d'assignation.

L'opérateur d'assignation pour les tableaux dans PHP fonctionne plus comme un clone paresseux. L'affectation d'une variable à une autre qui contient un tableau va le cloner, contrairement à la plupart des langues. Cependant, le clonage proprement dit ne sera pas effectué à moins que cela ne soit nécessaire. Cela signifie que le clone n'aura lieu que si l'une des variables est modifiée (copie sur écriture).

Voici un exemple:

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

En revenant à vos cas de test, vous pouvez facilement imaginer que foreach crée une sorte d'itérateur avec une référence au tableau. Cette référence fonctionne exactement comme la variable $b dans mon exemple. Cependant, l'itérateur avec la référence ne vit que pendant la boucle et ensuite, ils sont tous les deux ignorés. Maintenant, vous pouvez voir que, dans tous les cas sauf 3, le tableau est modifié pendant la boucle, alors que cette référence supplémentaire est active. Cela déclenche un clone, et cela explique ce qui se passe ici!

Voici un excellent article sur un autre effet secondaire de ce comportement de copie sur écriture: L’opérateur ternaire PHP: rapide ou pas?

109
linepogl

Quelques points à noter lorsque vous travaillez avec foreach():

a) foreach fonctionne sur la copie prospectée du tableau d'origine. Cela signifie que foreach() disposera d'un stockage de données SHARED jusqu'à ce qu'un prospected copy ne soit pas créé pour chaque commentaire/commentaire d'utilisateur .

b) Qu'est-ce qui déclenche une copie prospectée ? Une copie prospectée est créée en fonction de la stratégie de copy-on-write, c'est-à-dire chaque fois qu'un tableau transmis à foreach() est modifié, un clone du tableau d'origine est créé.

c) Le tableau d'origine et l'itérateur foreach() aura DISTINCT SENTINEL VARIABLES, c'est-à-dire un pour le tableau d'origine et un autre pour foreach; voir le code de test ci-dessous. SPL , Itérateurs , et Array Iterator .

Stack Overflow question Comment s'assurer que la valeur est réinitialisée dans une boucle 'foreach' en PHP? adresse les cas (3,4,5) de ta question.

L'exemple suivant montre que each () et reset () n'affectent PAS SENTINEL variables (for example, the current index variable) de l'itérateur 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/>";

Sortie:

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

NOTE POUR PHP 7

Pour mettre à jour cette réponse car elle a gagné en popularité: Cette réponse ne s'applique plus à partir de PHP 7. Comme expliqué dans la section " Modifications incompatibles avec la version précédente ", dans PHP 7, foreach fonctionne sur la copie du tableau. Ainsi, les modifications apportées au tableau lui-même ne sont pas répercutées sur la boucle foreach. Plus de détails sur le lien.

Explication (citation de php.net ):

La première forme boucle sur le tableau donné par array_expression. A chaque itération, la valeur de l'élément en cours est affectée à $ value et le pointeur de tableau interne est avancé d'un (ainsi, lors de la prochaine itération, vous regarderez l'élément suivant).

Ainsi, dans votre premier exemple, vous n’avez qu’un élément dans le tableau, et lorsque le pointeur est déplacé, l’élément suivant n’existe plus. Ainsi, après avoir ajouté un nouvel élément, chaque fin se termine car il a déjà "décidé" de le définir comme dernier élément.

Dans votre deuxième exemple, vous commencez avec deux éléments, et la boucle foreach n’est pas le dernier élément. Elle évalue le tableau à la prochaine itération et réalise donc qu’il y a un nouvel élément dans le tableau.

Je crois que tout cela est la conséquence de À chaque itération , une partie de l'explication de la documentation, ce qui signifie probablement que foreach effectue toute la logique avant il appelle le code dans {}.

Cas de test

Si vous exécutez ceci:

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

Vous obtiendrez cette sortie:

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

Ce qui signifie qu'il a accepté la modification et l'a appliquée car elle a été modifiée "à temps". Mais si vous faites ceci:

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

Tu auras:

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

Ce qui signifie que le tableau a été modifié, mais puisque nous l'avons modifié lorsque le foreach était déjà sur le dernier élément du tableau, il a "décidé" de ne plus boucler, et même si nous avons ajouté un nouvel élément, nous l'avons ajouté " trop tard "et il n'a pas été bouclé.

Des explications détaillées peuvent être lues à l’adresse suivante: . Comment fonctionne PHP 'foreach'? , ce qui explique les éléments internes à la base de ce comportement.

29
Damir Kasipovic

Selon la documentation fournie par PHP manual.

A chaque itération, la valeur de l’élément en cours est affectée à $ v et le paramètre interne
Le pointeur de tableau est avancé de un (ainsi, à la prochaine itération, vous regarderez l'élément suivant).

Donc, selon votre premier exemple:

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

$array n'a qu'un seul élément. Par conséquent, conformément à l'exécution foreach, 1 attribue à $v et il ne dispose d'aucun autre élément pour déplacer le pointeur.

Mais dans votre deuxième exemple:

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

$array possède deux éléments, donc $ array évalue les indices nuls et déplace le pointeur de un. Pour la première itération de la boucle, ajouté $array['baz']=3; comme passe par référence.

14
user3535130

Excellente question, car de nombreux développeurs, même expérimentés, sont désorientés par la façon dont PHP gère les tableaux dans des boucles foreach. Dans la boucle standard foreach, PHP crée une copie du tableau utilisé dans la boucle. La copie est supprimée immédiatement après la fin de la boucle. Ceci est transparent dans le fonctionnement d'une simple boucle foreach. Par exemple:

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

Cela génère:

Apple
banana
coconut

La copie est donc créée, mais le développeur ne le remarque pas, car le tableau d'origine n'est pas référencé dans la boucle ou après la fin de la boucle. Cependant, lorsque vous essayez de modifier les éléments d'une boucle, vous constatez qu'ils ne sont pas modifiés lorsque vous avez terminé:

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

print_r($set);

Cela génère:

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

Tout changement par rapport à l'original ne peut pas être un avis. En réalité, il n'y a aucun changement par rapport à l'original, même si vous avez clairement attribué une valeur à $ item. Cela est dû au fait que vous utilisez l'élément $ tel qu'il apparaît dans la copie de $ set en cours de traitement. Vous pouvez remplacer ceci en saisissant $ item par référence, comme suit:

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

Cela génère:

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

Il est donc évident et observable que, lorsque $ item est utilisé référence par référence, les modifications apportées à $ item sont apportées aux membres du jeu initial $. L'utilisation de $ item by reference empêche également PHP de créer la copie du tableau. Pour tester cela, nous allons d’abord montrer un script montrant la copie:

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

Cela génère:

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

Comme indiqué dans l'exemple, PHP a copié $ set et l'a utilisé pour la boucle, mais lorsque $ set a été utilisé à l'intérieur de la boucle, PHP a ajouté les variables au tableau d'origine. pas le tableau copié. Fondamentalement, PHP utilise uniquement le tableau copié pour l'exécution de la boucle et l'affectation de $ item. Pour cette raison, la boucle ci-dessus ne s'exécute que 3 fois et ajoute à chaque fois une autre valeur à la fin du jeu $ set d'origine, en laissant le jeu $ set d'origine avec 6 éléments, sans jamais entrer dans une boucle infinie.

Cependant, si nous avions utilisé $ item by reference, comme je l'ai déjà mentionné? Un seul caractère ajouté au test ci-dessus:

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

Résultats dans une boucle infinie. Notez qu’il s’agit en réalité d’une boucle infinie. Vous devrez soit supprimer le script vous-même, soit attendre que votre système d’exploitation manque de mémoire. J'ai ajouté la ligne suivante à mon script afin que PHP manque rapidement de mémoire, je vous suggère donc de faire de même si vous exécutez ces tests de boucle infinie:

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

Donc, dans cet exemple précédent avec la boucle infinie, nous voyons la raison pour laquelle PHP a été écrit pour créer une copie du tableau à boucler. Lorsqu'une copie est créée et utilisée uniquement par la structure de la construction de boucle elle-même, le tableau reste statique tout au long de l'exécution de la boucle. Vous ne rencontrerez donc jamais de problèmes.

11
hrvojeA

PHP boucle foreach peut être utilisé avec Indexed arrays, Associative arrays et Object public variables.

Dans la boucle foreach, la première chose que php fait est de créer une copie du tableau qui doit être itéré. PHP effectue ensuite une nouvelle itération sur cette nouvelle copy du tableau plutôt que sur celle d'origine. Ceci est démontré dans l'exemple ci-dessous:

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

De plus, php permet d'utiliser iterated values as a reference to the original array value. Ceci est démontré ci-dessous:

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

Remarque: Cela n'autorise pas l'utilisation de original array indexes comme references.

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

7
Pranav Rana