Question Comment fonctionne PHP 'foreach'?


Permettez-moi de préfixer cela en disant que je sais ce que foreach est, fait et comment l'utiliser. Cette question concerne la façon dont cela fonctionne sous le capot, et je ne veux pas de réponses dans le sens de "c'est comme ça que vous bouclez un tableau avec foreach".


Pendant longtemps, j'ai supposé que foreach travaillé 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 j'ai récemment entamé une discussion à ce sujet, et après un peu d'expérimentation j'ai découvert que ce n'était pas vrai à 100%.

Laisse-moi te montrer ce que je veux dire. Pour les cas de test suivants, nous allons travailler avec 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 - sinon la boucle continuerait indéfiniment, car nous poussons constamment des éléments sur le 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 */

Cela sauvegarde notre conclusion initiale, nous travaillons avec une copie du tableau source pendant la boucle, sinon nous verrons les valeurs modifiées pendant la boucle. Mais...

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

Lorsque foreach commence à s'exécuter, le pointeur interne du tableau est automatiquement réinitialisé sur le premier élément du tableau.

Droit ... cela semble suggérer que foreach repose sur le pointeur de tableau du tableau source. Mais nous avons juste prouvé que nous sommes ne fonctionne pas avec le tableau source, droite? Eh bien, pas entièrement.

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

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

Le manuel de PHP indique également:

Comme foreach s'appuie sur le pointeur de tableau interne en le modifiant dans la boucle peut conduire à un comportement inattendu.

Eh bien, découvrons 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à-dedans, en fait il semble soutenir la théorie de la "copie de la source".


La question

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

Il me semble que foreach travaille avec un 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?
  • Y at-il une situation où l'utilisation de fonctions qui ajustent le pointeur de tableau (each(), reset() et al.) au cours d'une foreach pourrait affecter le résultat de la boucle?

1637
2018-04-07 19:33


origine


Réponses:


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

Dans la suite, j'essaierai d'expliquer précisément comment fonctionne l'itération dans les différents cas. De loin le cas le plus simple est Traversable objets, quant à ceux-ci foreach est essentiellement la seule syntaxe du sucre pour le code le long de ces lignes:

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 ne fait que refléter Iterator interface sur le niveau C.

L'itération des tableaux et des objets simples est considérablement plus compliquée. Tout d'abord, il faut noter qu'en PHP les "tableaux" sont des dictionnaires vraiment ordonnés et ils seront traversés selon cet ordre (qui correspond à l'ordre d'insertion tant que vous n'utilisez pas quelque chose comme sort). Cela s'oppose à l'itération par l'ordre naturel des clés (comme les listes dans d'autres langues fonctionnent souvent) ou à l'absence d'ordre défini (comme les dictionnaires dans d'autres langues fonctionnent souvent).

La même chose s'applique également aux objets, car les propriétés de l'objet peuvent être vues comme des noms de propriétés de mappage de dictionnaire (ordonné) à leurs valeurs, plus une certaine gestion de la 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. Cependant, si vous commencez à itérer sur un objet, la représentation condensée normalement utilisée sera convertie en un vrai dictionnaire. À ce stade, l'itération des objets simples devient très similaire à l'itération des tableaux (ce qui explique pourquoi je ne parle pas ici de l'itération des objets simples).

Jusqu'ici tout va bien. Itérer sur un dictionnaire ne peut pas être trop dur, non? Les problèmes commencent lorsque vous réalisez qu'un tableau / objet peut changer pendant l'itération. Il y a plusieurs façons de le faire:

  • Si vous itérez par référence en utilisant foreach ($arr as &$v) puis $arr est transformé en référence et vous pouvez le changer pendant l'itération.
  • En PHP 5, la même chose s'applique même si vous itérez par valeur, mais le tableau était une référence à l'avance: $ref =& $arr; foreach ($ref as $v)
  • Les objets ont une sémantique de passage de type by-handle, ce qui, pour des raisons pratiques, signifie qu'ils se comportent comme des références. Ainsi, les objets peuvent toujours être modifiés pendant l'itération.

Le problème avec l'autorisation de modifications pendant l'itération est le cas où l'élément sur lequel vous êtes actuellement est supprimé. Supposons que vous utilisiez un pointeur pour suivre l'élément du tableau dans lequel vous vous trouvez. Si cet élément est maintenant libéré, il vous reste un pointeur qui pend (ce qui entraîne généralement un segfault).

Il existe différentes façons de résoudre ce problème. PHP 5 et PHP 7 diffèrent de manière significative à cet égard et je décrirai les deux comportements dans ce qui suit. Le résumé est que l'approche de PHP 5 était plutôt stupide et conduisait à toutes sortes de problèmes bizarres, tandis que l'approche plus impliquée de PHP 7 aboutissait à un comportement plus prévisible et cohérent.

En dernier lieu, 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). Seulement une fois que vous effectuez une sorte de modification, une copie réelle (appelée "duplication") sera effectuée. Voir Vous êtes menti à pour une introduction plus complète sur ce sujet.

PHP 5

Pointeur de tableau interne et HashPointer

Les tableaux dans PHP 5 ont un "pointeur de tableau interne" (IAP) dédié, qui supporte correctement les modifications: Chaque fois qu'un élément est supprimé, il vérifie si l'IAP pointe vers cet élément. Si c'est le cas, il est avancé à l'élément suivant à la place.

Alors que foreach utilise l'IAP, il y a une complication supplémentaire: Il n'y a qu'un IAP, 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 schenanigans suivants: Avant l'exécution du corps de la boucle, foreach sauvegarde un pointeur sur l'élément en cours et son hachage sur une per-foreach HashPointer. Après l'exécution du corps de la boucle, l'IAP sera rétabli sur cet élément s'il existe toujours. Si toutefois l'élément a été supprimé, nous n'utiliserons que l'IAP actuellement. Ce système fonctionne plutôt bien, mais il y a beaucoup de comportements bizarres, dont je vais vous montrer ci-dessous.

Duplication de tableau

L'IAP est une caractéristique visible d'un tableau (exposé à travers le current famille de fonctions), en tant que telles, les changements apportés à l'IAP comptent comme des modifications dans la sémantique de copie à l'écriture. Cela signifie malheureusement que foreach est dans de nombreux cas obligé de dupliquer le tableau qu'il parcourt. 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 changements sont supposé propager, donc il ne devrait pas être dupliqué.
  2. Le tableau a refcount> 1. Si refcount vaut 1, alors 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 foreach par référence est utilisé, le tableau (potentiellement dupliqué) sera transformé en référence.

Considérez ce code comme un exemple où la duplication se produit:

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

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

Ici, $arr sera dupliqué pour empêcher les changements IAP sur $arr de fuite à $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 un artefact de la mise en œuvre sous-optimale (il n'y a pas de souci de modification lors de l'itération ici, donc nous n'avons pas vraiment besoin d'utiliser l'IAP en premier lieu).

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

Position d'avancement

Il existe un dernier détail d'implémentation dont vous devez tenir compte pour bien comprendre les exemples de code ci-dessous. La manière "normale" de faire une boucle dans une structure de données ressemblerait à ceci en pseudocode:

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

toutefois foreach, étant un flocon de neige plutôt spécial, 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 de tableau est déjà avancé avant le corps de la boucle fonctionne. Cela signifie que pendant que le corps de la boucle travaille sur l'élément $i, l'IAP est déjà à l'élément $i+1. C'est la raison pour laquelle les échantillons de code présentant des modifications au cours de l'itération prochain élément, plutôt que l'actuel.

Exemples: vos cas de test

Les trois aspects décrits ci-dessus devraient vous donner une impression presque complète des particularités de l'implémentation de foreach et nous pouvons passer à quelques exemples.

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

  • Dans les cas de test 1 et 2 $array commence par refcount = 1, donc il ne sera pas dupliqué par foreach: Seul le refcount est incrémenté. Lorsque le corps de la boucle modifie ensuite le tableau (qui a refcount = 2 à ce point), la duplication se produira à ce point. Foreach continuera à travailler sur une copie non modifiée de $array.

  • Dans le scénario de test 3, une fois de plus, le tableau n'est pas dupliqué, donc foreach modifiera le IAP du $array variable. A la fin de l'itération, l'IAP est NULL (signifiant itération), qui each indique en retournant false.

  • Dans les cas de test 4 et 5, les deux each et reset sont des fonctions de 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 est d'observer le comportement du current() fonctionner à l'intérieur d'une boucle foreach. Considérez cet exemple:

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

Ici, vous devriez savoir que current() est une fonction by-ref (en fait: prefer-ref), même si elle ne modifie pas le tableau. Il doit être dans le but de jouer bien avec toutes les autres fonctions comme next qui sont tous par-ref. Le passage par référence implique que le tableau doit être séparé et donc $array et le tableau foreach sera différent. La raison pour laquelle vous obtenez 2 au lieu de 1 est également mentionné ci-dessus: foreach avance le pointeur du tableau avant l'exécution du code utilisateur, pas après. Donc même si le code est au premier élément, foreach a déjà avancé le pointeur vers le second.

Maintenant essayons 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é (comme ci-dessus). Mais maintenant que c'est une référence, le tableau ne doit plus être dupliqué lors du passage au by-ref current() fonction. Ainsi current() et foreach travail sur le même tableau. Vous voyez encore le comportement off-by-one, en raison de la façon dont foreachavance le pointeur.

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

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

Ici, la partie importante est que foreach fera $array un is_ref = 1 quand il est itéré par référence, donc en gros vous avez 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 */

Voici le refcount du $array est de 2 lorsque la boucle est lancée, donc pour une fois nous devons faire la duplication à l'avance. Ainsi $array et le tableau utilisé par foreach sera complètement séparé du début. C'est pourquoi vous obtenez la position de l'IAP partout où il était avant la boucle (dans ce cas, il était à la première position).

Exemples: Modification pendant l'itération

Essayer de tenir compte des modifications au cours de l'itération est l'origine de tous nos problèmes de foreach, donc il sert à considérer quelques exemples pour ce cas.

Considérez ces boucles imbriquées sur le même tableau (où l'itération by-ref est utilisée pour s'assurer que c'est vraiment la 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 celle (1, 2) est manquant à la sortie, car l'élément 1 a été éliminé. Ce qui est probablement inattendu est que la boucle externe s'arrête après le premier élément. Pourquoi donc?

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

Une autre conséquence de HashPointer mécanisme de sauvegarde + restauration est que les changements à l'IAP si reset() etc. n'affectent généralement pas foreach. Par exemple, le code suivant s'exécute comme si le reset() n'étaient pas présents 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, tout en reset() modifie temporairement l'IAP, il sera restauré dans l'élément foreach actuel après le corps de la boucle. Forcer reset() Pour effectuer un effet sur la boucle, vous devez en outre supprimer l'élément en cours, de sorte 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 toujours sensés. Le vrai plaisir commence si vous vous souvenez que le HashPointer restore 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 des touches de tableau, nous pouvons faire foreach croire qu'un élément qui a été supprimé existe toujours, donc il va directement y accéder. 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 attendre la sortie 1, 1, 3, 4 selon les règles précédentes. Comment ça se passe? 'FYFY' a le même hachage que l'élément supprimé 'EzFY', et l'allocateur arrive à réutiliser le même emplacement de mémoire pour stocker l'élément. Donc foreach finit directement par sauter sur l'élément nouvellement inséré, raccourcissant ainsi la boucle.

Substitution de 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. Ainsi, vous pouvez démarrer l'itération sur un tableau, puis le remplacer par un autre tableau à mi-chemin. Ou commencez l'itération sur un tableau et 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 voir dans ce cas PHP commencera juste à répéter l'autre entité dès le début une fois la substitution effectuée.

PHP 7

Itérateurs Hashtable

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

PHP 7 utilise une approche différente, à savoir qu'il supporte la création d'une quantité arbitraire d'itérateurs de hashtable externes et sécurisés. Ces itérateurs doivent être enregistrés 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 pointant vers cet élément seront avancés vers l'élément suivant.

Cela signifie que foreach n'utilisera plus l'IAP du tout. 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 comme reset() etc.

Duplication de tableau

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 du tableau par valeur ne fera qu'un incrément de refcount (au lieu de dupliquer le tableau) dans tous les cas. Si le tableau est modifié pendant la boucle foreach, à ce stade une duplication se produira (selon copy-on-write) et foreach continuera à travailler sur l'ancien tableau.

Dans la plupart des cas, cette modification est transparente et n'a d'autre effet qu'une meilleure performance. Cependant, il y a une occasion où il en résulte un comportement différent, à savoir le cas où le tableau était une référence à l'avance:

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

L'itération précédente des valeurs de référence était un cas particulier. Dans ce cas, aucune duplication ne s'est produite, donc toutes les modifications du tableau pendant l'itération seront reflétées par la boucle. En PHP 7, ce cas particulier a disparu: une itération par valeur d'un tableau toujours continuer à travailler sur les éléments d'origine, en ne tenant compte d'aucune modification pendant la boucle.

Ceci, bien sûr, ne s'applique pas à l'itération par référence. Si vous itérez 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 des objets par manipula- tion (c'est-à-dire qu'ils se comportent comme des références même dans des contextes par valeur).

Exemples

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

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

  • Le scénario 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() va dupliquer le tableau avant de changer l'IAP, tandis que foreach utilise toujours le tableau original. (Ce n'est pas que le changement IAP aurait eu de l'importance, même si le tableau a été partagé.)

Le deuxième ensemble d'exemples était lié au comportement de current() sous différentes configurations de référence / référence. Cela n'a plus de sens, comme current() est complètement non affecté par la boucle, donc sa valeur de retour reste toujours la même.

Cependant, nous obtenons quelques changements intéressants en considérant des modifications pendant 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 voir, 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 hachage entièrement distincts, et qu'il n'y a plus de contamination croisée des deux boucles via un IAP partagé.

Un autre cas de bord étrange qui est maintenant résolu, est l'effet étrange que vous obtenez lorsque vous supprimez et ajoutez des éléments qui ont le même hachage:

$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 sautait directement vers le nouvel élément, car il «ressemblait» à l'élément remove (en raison de la collision entre le hachage et le pointeur). Comme nous ne comptons plus sur le hachage de l'élément, ce n'est plus un problème.


1378
2018-02-13 13:21



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'affectation.

L'opérateur d'affectation pour les tableaux en PHP fonctionne plus comme un clone paresseux. Assigner une variable à une autre qui contient un tableau clone le tableau, contrairement à la plupart des langues. Cependant, le clonage réel ne sera pas fait à moins que ce soit nécessaire. Cela signifie que le clone n'aura lieu que lorsque 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.

Pour revenir à 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 deux mis au rebut. 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 vivante. Cela déclenche un clone, et cela explique ce qui se passe ici!

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


97
2018-04-07 20:43



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

une) foreach travaille sur le copie prospecte du tableau original.     Cela signifie foreach () aura stockage de données partagées jusqu'à ou à moins prospected copy est     pas créé foreach Notes / Commentaires d'utilisateurs.

b) Qu'est-ce qui déclenche un copie prospecte?     La copie prospecte est créée en fonction de la politique de copy-on-writec'est-à-dire à chaque fois     un tableau passé à foreach () est modifié, un clone de tableau d'origine est créé.

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

Stack Overflow question Comment s'assurer que la valeur est réinitialisée dans une boucle 'foreach' en PHP? traite les cas (3,4,5) de votre 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

34
2018-04-07 21:03



NOTE POUR PHP 7

Pour mettre à jour cette réponse car elle a gagné en popularité: Cette réponse ne s'applique plus depuis PHP 7. Comme expliqué dans le "Modifications incompatibles vers l'arrière", en PHP 7 foreach fonctionne sur la copie du tableau, donc les changements sur le tableau lui-même ne sont pas reflétés 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. Sur chaque   itération, la valeur de l'élément courant est affectée à $ value et   le pointeur interne du tableau est avancé par un (donc le suivant   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 pas, donc après avoir ajouté un nouvel élément pour chaque extrémité, car il l'a déjà "décidé" comme dernier élément.

Dans votre deuxième exemple, vous commencez avec deux éléments, et foreach loop n'est pas au dernier élément, donc il évalue le tableau à l'itération suivante et réalise ainsi qu'il y a un nouvel élément dans le tableau.

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

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 traversée parce qu'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à au dernier élément du tableau, il a "décidé" de ne plus faire de boucle, et même si nous avons ajouté un nouvel élément, nous l'avons ajouté "trop ​​tard" et il n'a pas été bouclé.

L'explication détaillée peut être lue à Comment fonctionne PHP 'foreach'? ce qui explique les internes derrière ce comportement.


22
2018-04-15 08:46



Selon la documentation fournie par le manuel PHP.

A chaque itération, la valeur de l'élément en cours est assignée à $ v et l'interne
  Le pointeur de tableau est avancé de un (donc à l'itération suivante, 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 avoir seulement un seul élément, de sorte que, selon l'exécution foreach, 1 attribuer à $vet il n'a 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 avoir deux éléments, alors $ array évalue les indices zéro 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.


8
2018-04-15 09:32



Bonne question, car beaucoup de développeurs, même expérimentés, sont confus par la façon dont PHP gère les tableaux dans les boucles foreach. Dans la boucle foreach standard, PHP crée une copie du tableau utilisé dans la boucle. La copie est rejeté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 produit:

apple
banana
coconut

Ainsi, la copie est créée mais le développeur ne s'en aperçoit pas, car le tableau d'origine n'est pas référencé dans la boucle ou après la fin de la boucle. Toutefois, lorsque vous essayez de modifier les éléments dans 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 produit:

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

Toutes les modifications par rapport à l'original ne peuvent pas être des notifications, en fait, il n'y a aucun changement par rapport à l'original, même si vous avez clairement affecté une valeur à $ item. C'est parce que vous travaillez sur $ item 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 ceci:

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

Cela produit:

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

Il est donc évident et observable, lorsque $ item est opéré par référence, les changements apportés à $ item sont faits aux membres du $ set original. 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 rapide démontrant la copie:

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

Cela produit:

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

Comme il est montré dans l'exemple, PHP a copié $ set et l'a utilisé pour faire une boucle, mais quand $ set a été utilisé dans la boucle, PHP a ajouté les variables au tableau original, pas au tableau copié. Fondamentalement, PHP n'utilise que 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 chaque fois qu'elle ajoute une autre valeur à la fin de l'ensemble $ original, en laissant l'original $ set avec 6 éléments, mais n'entre jamais dans une boucle infinie.

Cependant, que se serait-il passé si nous avions utilisé $ item par référence, 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 fait d'une boucle infinie, vous devrez soit tuer le script vous-même ou attendre que votre système d'exploitation manque de mémoire. J'ai ajouté la ligne suivante à mon script afin que PHP manque de mémoire très rapidement, je vous suggère de faire la même chose si vous allez exécuter 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 la boucle elle-même, le tableau reste statique pendant toute l'exécution de la boucle, donc vous ne rencontrerez jamais de problèmes.


5
2018-04-21 08:44



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

Dans foreach loop, la première chose que fait php est qu'il crée une copie du tableau qui doit être répété. PHP puis itère sur ce nouveau copy du tableau plutôt que l'original. 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).

En outre, PHP permet d'utiliser iterated values as a reference to the original array value ainsi que. 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: Il ne permet pas original array indexes à utiliser comme references.

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


5
2017-11-13 14:08