Comment fonctionne PHP foreach?

2019

Permettez-moi de préfixer cela en disant que je sais ce que foreachc'est, ce qu'il 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 du type "c'est comme ça que vous bouclez un tableau avec foreach".


Pendant longtemps, j'ai supposé que cela foreachfonctionnait avec le tableau lui-même. Ensuite, j'ai trouvé de nombreuses références au fait qu'il fonctionne avec une 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%.

Permettez-moi de montrer ce que je veux dire. Pour les cas de test suivants, nous travaillerons 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 pour toujours, 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 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 à s'exécuter pour la première fois, le pointeur de tableau interne est automatiquement réinitialisé sur le premier élément du tableau.

Bon ... cela semble suggérer que foreachrepose sur le pointeur de tableau du tableau source. Mais nous venons de prouver que nous ne travaillons pas avec le tableau source , non? Enfin, 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, le cas de test 1 serait en boucle pour toujours.

Le manuel PHP indique également:

Comme foreach repose sur le pointeur de tableau interne, le modifier dans la boucle peut entraîner 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à-bas, 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 conclusion correcte simplement en regardant le code source PHP, j'apprécierais que quelqu'un puisse le traduire en anglais pour moi.

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

  • Est-ce correct et toute l'histoire?
  • Sinon, que fait-il vraiment?
  • Existe-t-il une situation où l'utilisation de fonctions qui ajustent le pointeur de tableau ( each(), reset()et al.) Pendant a foreachpourrait affecter le résultat de la boucle?
DaveRandom
la source
5
@DaveRandom Il y a probablement une balise php-internals , mais je vous laisse le soin de décider laquelle des 5 autres balises remplacer.
Michael Berkowski
5
ressemble à COW, sans supprimer la poignée
zb '
149
Au début, j'ai pensé »mon Dieu, une autre question pour les débutants. Lisez la documentation… hm, comportement clairement indéfini ». Ensuite, j'ai lu la question complète, et je dois dire: j'aime ça. Vous y avez mis un certain effort et écrit tous les tests. ps. les tests 4 et 5 sont-ils les mêmes?
knittl
21
Juste une idée de la raison pour laquelle il est logique que le pointeur de tableau soit touché: PHP doit réinitialiser et déplacer le pointeur de tableau interne du tableau d'origine avec la copie, car l'utilisateur peut demander une référence à la valeur actuelle ( foreach ($array as &$value)) - PHP a besoin de connaître la position actuelle dans le tableau d'origine même s'il s'agit en fait d'une itération sur une copie.
Niko
4
@Sean: À mon humble avis, la documentation PHP est vraiment assez mauvaise pour décrire les nuances des fonctionnalités du langage de base. Mais c'est peut-être parce que tant de cas spéciaux ad hoc sont intégrés dans la langue ...
Oliver Charlesworth

Réponses:

1660

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

Dans ce qui suit, je vais essayer d'expliquer précisément comment fonctionne l'itération dans différents cas. Le cas de loin le plus simple est celui des Traversableobjets, car il ne foreachs'agit pour l'essentiel que de sucre de syntaxe pour le code suivant:

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' Iteratorinterface au niveau C.

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

La même chose s'applique également aux objets, car les propriétés d'objet peuvent être vues comme un autre dictionnaire (ordonné) mappant les noms de propriété à leurs valeurs, plus une certaine gestion de la visibilité. Dans la majorité des cas, les propriétés des objets 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 compressée normalement utilisée sera convertie en un véritable dictionnaire. À ce stade, l'itération d'objets simples devient très similaire à l'itération de tableaux (c'est pourquoi je ne parle pas beaucoup de l'itération d'objets simples ici).

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 itérez par référence en utilisant foreach ($arr as &$v)puis $arrest 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 au préalable: $ref =& $arr; foreach ($ref as $v)
  • Les objets ont un by-handle passant la sémantique, ce qui signifie pour la plupart des cas pratiques 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 des modifications pendant l'itération est le cas où l'élément sur lequel vous êtes actuellement est supprimé. Supposons que vous utilisez un pointeur pour savoir à quel élément du tableau vous vous trouvez actuellement. Si cet élément est maintenant libéré, vous vous retrouvez avec un pointeur suspendu (ce qui entraîne généralement 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-dessous. Le résumé est que l'approche de PHP 5 était plutôt stupide et conduisait à toutes sortes de problèmes de bord étranges, tandis que l'approche plus impliquée de PHP 7 se traduisait par un comportement plus prévisible et cohérent.

En dernier lieu, il convient de noter que PHP utilise le comptage des références et la copie sur écriture pour gérer la mémoire. Cela signifie que si vous "copiez" une valeur, vous ne faites que réutiliser l'ancienne valeur et incrémenter son compte de référence (refcount). Une fois que vous avez effectué une sorte de modification, une copie réelle (appelée "duplication") sera effectuée. Voir On vous ment pour une introduction plus complète sur ce sujet.

PHP 5

Pointeur de tableau interne et HashPointer

Les tableaux en 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 y aura une vérification si l'IAP pointe vers cet élément. Si c'est le cas, il est avancé à l'élément suivant à la place.

Bien foreachqu'il utilise l'IAP, il existe une complication supplémentaire: il n'y a qu'un seul IAP, mais un tableau peut faire partie de plusieurs foreachboucles:

// 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, foreacheffectuez les manœuvres suivantes: Avant l'exécution du corps de la boucle, foreachsauvegardera un pointeur sur l'élément actuel et son hachage dans un per-foreach HashPointer. Après l'exécution du corps de la boucle, l'IAP sera redéfini sur cet élément s'il existe toujours. Si toutefois l'élément a été supprimé, nous n'utiliserons que l'endroit où se trouve actuellement l'IAP. Ce schéma fonctionne principalement en quelque sorte, mais il y a beaucoup de comportements étranges que vous pouvez en tirer, dont certains que je vais démontrer ci-dessous.

Duplication de baies

L'IAP est une caractéristique visible d'un tableau (exposée à travers la currentfamille de fonctions), car de telles modifications du compte IAP comptent comme des modifications dans la sémantique de copie sur écriture. Cela signifie malheureusement que, foreachdans de nombreux cas, il est obligé de dupliquer le tableau sur lequel il est en cours d'itération. Les conditions précises sont:

  1. Le tableau n'est pas une référence (is_ref = 0). S'il s'agit d'une référence, les modifications qui y sont apportées sont censées se propager et ne doivent donc pas être dupliquées.
  2. Le tableau a refcount> 1. Si refcountvaut 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), alors seul son refcountsera incrémenté (*). De plus, si foreachpar référence est utilisé, le tableau (potentiellement dupliqué) sera transformé en 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, $arrsera dupliqué pour éviter les $arrfuites des modifications IAP $outerArr. En termes des 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 l'implémentation sous-optimale (il n'y a pas de souci de modification pendant l'itération ici, donc nous n'avons pas vraiment besoin d'utiliser l'IAP en premier lieu).

(*) L'incrémentation refcountici semble inoffensive, mais viole la sémantique de copie sur écriture (COW): cela signifie que nous allons modifier l'IAP d'un tableau refcount = 2, tandis que COW stipule 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 qu'un COW est normalement transparent) car le changement 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) ne pas incrémenter le refcountet ainsi permettre au tableau itéré d'être arbitrairement modifié dans la boucle ou c) ne pas utiliser du tout l'IAP (le PHP 7 solution).

Ordre d'avancement de poste

Il y a un dernier détail d'implémentation que vous devez connaître pour bien comprendre les exemples de code ci-dessous. La manière "normale" de parcourir une certaine 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 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 l'exécution du corps de boucle. Cela signifie que pendant que le corps de la boucle travaille sur l'élément $i, l'IAP est déjà sur l'élément $i+1. C'est la raison pour laquelle les échantillons de code montrant une modification pendant l'itération seront toujours unsetl' élément suivant , plutôt que l'élément 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' foreachimplémentation 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 $arraycommence par refcount = 1, il ne sera donc pas dupliqué par foreach: Seul le refcountest incrémenté. Lorsque le corps de boucle modifie par la suite le tableau (qui a refcount = 2 à ce point), la duplication se produit à ce point. Foreach continuera de travailler sur une copie non modifiée de $array.

  • Dans le cas de test 3, une fois de plus, le tableau n'est pas dupliqué, ce foreachqui modifiera donc l'IAP de la $arrayvariable. À la fin de l'itération, l'IAP est NULL (ce qui signifie que l'itération a été effectuée), ce qui eachindique en retournant false.

  • Dans les cas de test 4 et 5, eachil resets'agit de fonctions de référence. Le $arraya un refcount=2quand il leur est transmis, il doit donc être dupliqué. En tant que tel, foreachil travaillera à nouveau sur un tableau séparé.

Exemples: effets de currentin foreach

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

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

Ici, vous devez savoir qu'il current()s'agit d'une fonction by-ref (en fait: prefer-ref), même si elle ne modifie pas le tableau. Cela doit être pour jouer bien avec toutes les autres fonctions comme celles nextqui sont toutes by-ref. Le passage par référence implique que le tableau doit être séparé et donc $arrayet le foreach-arraysera différent. La raison pour laquelle vous obtenez à la 2place de 1est également mentionnée ci-dessus: foreachavance le pointeur de tableau avant d' exécuter le code utilisateur, pas après. Ainsi, même si le code est au premier élément, foreachle pointeur a déjà été avancé au 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é (comme ci-dessus). Mais maintenant qu'il s'agit d'une référence, le tableau n'a plus à être dupliqué lors du passage à la fonction by-ref current(). Ainsi current()et foreachtravaillez sur le même tableau. Cependant, vous voyez toujours le comportement off-by-one, en raison de la façon dont foreachle pointeur avance.

Vous obtenez le même comportement lors de l'itération par 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 $arrayun is_ref = 1 lorsqu'il est itéré par référence, donc fondamentalement, 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 */

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

Exemples: modification pendant l'itération

Essayer de prendre en compte les modifications pendant l'itération est à l'origine de tous nos problèmes foreach, donc cela 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 qu'elle 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 qui (1, 2)manque dans la sortie car l'élément a 1é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 cela est le hack de boucle imbriquée 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 a HashPointer. Après le corps de la boucle, il sera restauré, mais uniquement 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 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 HashPointermécanisme de sauvegarde + restauration est que les modifications apportées à l'IAP via reset()etc. n'ont généralement pas d'impact foreach. Par exemple, le code suivant s'exécute comme s'il 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 qu'il reset()modifie temporairement l'IAP, il sera restauré dans l'élément foreach actuel après le corps de la boucle. Pour forcer reset()à faire un effet sur la boucle, vous devez en outre 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 toujours sains d'esprit. Le vrai plaisir commence si vous vous souvenez que la HashPointerrestauration 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 qu'avec un choix judicieux de clés de tableau, nous pouvons faire foreachcroire qu'un élément qui a été supprimé existe toujours, il y sautera donc 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 attendre la sortie 1, 1, 3, 4selon les règles précédentes. Ce qui se passe, c'est qu'il '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. Ainsi, foreach finit par sauter directement à 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 remplacer l'entité itérée pendant la boucle. Vous pouvez donc commencer l'itération sur un tableau, puis le remplacer par un autre tableau à mi-chemin. 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 voir dans ce cas, PHP commencera juste à itérer l'autre entité depuis le début une fois la substitution effectuée.

PHP 7

Itérateurs de table de hachage

Si vous vous souvenez encore, le principal problème avec l'itération de tableau était de savoir comment gérer la suppression des éléments à mi-itération. PHP 5 a utilisé un seul pointeur de tableau interne (IAP) à cet effet, qui était quelque peu sous-optimal, car un pointeur de tableau devait être étiré pour prendre en charge plusieurs boucles foreach simultanées et l' 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'une quantité arbitraire d'itérateurs de table de hachage externes et sûrs. Ces itérateurs doivent être enregistrés dans le tableau, à partir de ce moment, ils ont la même sémantique que l'IAP: si un élément du tableau est supprimé, tous les itérateurs de table de hachage pointant vers cet élément seront avancés vers l'élément suivant.

Cela signifie que foreachn'utilisera plus du tout l'IAP . La foreachboucle 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 baies

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 ne fera qu'un refcountincrément (au lieu de dupliquer le tableau) dans tous les cas. Si le tableau est modifié pendant la foreachboucle, à ce stade, une duplication se produira (selon la copie sur écriture) et foreachcontinuera de fonctionner sur l'ancien tableau.

Dans la plupart des cas, ce changement est transparent et n'a 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 une référence au préalable:

$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 des tableaux de référence était des cas spéciaux. Dans ce cas, aucune duplication ne s'est produite, donc toutes les modifications du tableau pendant l'itération seraient reflétées par la boucle. En PHP 7, ce cas particulier a disparu: une itération par valeur d'un tableau continuera toujours à travailler 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 parcourez 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 du by-handle des objets (c'est-à-dire qu'ils se comportent comme des références même dans des contextes de by-value).

Exemples

Prenons 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 de tableau par valeur continue de fonctionner sur les éléments d'origine. (Dans ce cas, le refcountingcomportement pair et de duplication est exactement le même entre PHP 5 et PHP 7).

  • Le scénario de test 3 change: Foreachn'utilise plus l'IAP, il each()n'est donc 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 la baie avant de modifier l'IAP, tout foreachen utilisant toujours la baie d'origine. (Ce n'est pas que le changement IAP aurait eu de l'importance, même si le tableau était partagé.)

Le deuxième ensemble d'exemples était lié au comportement de current()sous différentes reference/refcountingconfigurations. Cela n'a plus de sens, car il current()n'est pas affecté par la boucle, donc sa valeur de retour reste toujours la même.

Cependant, nous obtenons des changements intéressants lors de l'examen 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 voir, la boucle externe n'interrompt 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 il n'y a plus de contamination croisée des deux boucles via un IAP partagé.

Un autre cas de bord étrange qui est résolu maintenant, est l'effet étrange que vous obtenez lorsque vous supprimez et ajoutez des éléments qui se trouvent avoir 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" à la même chose que l'élément supprimé (en raison de la collision du hachage et du pointeur). Comme nous ne comptons plus sur l'élément de hachage pour rien, ce n'est plus un problème.

NikiC
la source
4
@Baba Oui. Le passer à une fonction revient à le faire $foo = $arrayavant la boucle;)
NikiC
32
Pour ceux d'entre vous qui ne savent pas ce qu'est un zval, veuillez consulter le blog de
shu zOMG chen
1
Correction mineure: ce que vous appelez Bucket n'est pas ce qui est normalement appelé Bucket dans une table de hachage. Normalement, Bucket est un ensemble d'entrées avec la même taille de hachage%. Vous semblez l'utiliser pour ce qu'on appelle normalement une entrée. La liste liée n'est pas sur les compartiments, mais sur les entrées.
unbeli
12
@unbeli J'utilise la terminologie utilisée en interne par PHP. Les Buckets font partie d'une liste doublement liée pour les collisions de hachage et font également partie d'une liste doublement liée pour la commande;)
NikiC
4
Grande réponse. Je pense que tu voulais dire iterate($outerArr);et pas iterate($arr);quelque part.
niahoo
116

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 en ce qui concerne les tableaux PHP 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. L'affectation d'une variable à une autre contenant un tableau clone le tableau, contrairement à la plupart des langues. Cependant, le clonage proprement dit ne sera effectué que s'il est 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.

Pour en revenir à vos cas de test, vous pouvez facilement imaginer que cela foreachcré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 et la référence ne vivent que pendant la boucle, puis ils sont tous les deux rejetés. Vous pouvez maintenant voir que, dans tous les cas sauf 3, le tableau est modifié pendant la boucle, tandis 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 pour un autre effet secondaire de ce comportement de copie sur écriture: L'opérateur ternaire PHP: rapide ou non?

linepogl
la source
semble votre droit, j'ai fait un exemple qui démontre que: codepad.org/OCjtvu8r une différence de votre exemple - il ne copie pas si vous changez la valeur, seulement si changez les clés.
zb '
Cela explique en effet tout le comportement montré ci-dessus, et cela peut être bien illustré en appelant each()à la fin du premier cas de test, où nous voyons que le pointeur de tableau du tableau d'origine pointe vers le deuxième élément, puisque le tableau a été modifié pendant la première itération. Cela semble également démontrer que foreachdéplace le pointeur de tableau avant d'exécuter le bloc de code de la boucle, ce à quoi je ne m'attendais pas - j'aurais pensé qu'il ferait cela à la fin. Merci beaucoup, cela m'éclaircit bien.
DaveRandom
49

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

a) foreachfonctionne sur la copie prospectée de la matrice d'origine. Cela signifie qu'il y foreach()aura un stockage de données PARTAGÉ jusqu'à ce que ou à moins qu'aucunprospected copy ne soit créé pour chaque note / commentaire utilisateur .

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

c) Le tableau et l' foreach()itérateur d'origine auront 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 , Iterators et Array Iterator .

Question de dépassement de pile Comment s'assurer que la valeur est réinitialisée dans une boucle «foreach» en PHP? répond aux cas (3,4,5) de votre question.

L'exemple suivant montre que each () et reset () N'affectent PAS les SENTINELvariables (for example, the current index variable)de l' foreach()itérateur.

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

Production:

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
sakhunzai
la source
2
Votre réponse n'est pas tout à fait correcte. foreachfonctionne sur une copie potentielle du tableau, mais il ne fait pas la copie réelle, sauf si elle est nécessaire.
linepogl
aimeriez-vous montrer comment et quand cette copie potentielle est créée par le code? Mon code montre que foreachcopie le tableau 100% du temps. J'ai hâte de savoir. Merci pour vos commentaires
sakhunzai
La copie d'un tableau coûte cher. Essayez de compter le temps qu'il faut pour parcourir un tableau de 100 000 éléments à l'aide de forou foreach. Vous ne verrez aucune différence significative entre les deux, car une copie réelle n'a pas lieu.
linepogl
Ensuite, je suppose qu'il y a SHARED data storageréservé jusqu'à ou à moins copy-on-write, mais (d'après mon extrait de code), il est évident qu'il y aura toujours DEUX ensembles de l' SENTINEL variablesun pour l' original arrayautre et pour foreach. Merci qui a du sens
sakhunzai
1
oui c'est une copie "prospectée" c'est-à-dire une copie "potentielle". Elle n'est pas protégée comme vous l'avez suggéré
sakhunzai
33

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 le " modifications incompatibles en amont ", dans PHP 7 foreach fonctionne sur la copie du tableau, donc tout changement 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 ):

Le premier formulaire boucle sur le tableau donné par array_expression. À chaque itération, la valeur de l'élément actuel est affectée à $ value et le pointeur de tableau interne est avancé d'une unité (donc à la prochaine itération, vous regarderez l'élément suivant).

Ainsi, dans votre premier exemple, vous n'avez qu'un seul é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 fin, car il a déjà "décidé" de le placer comme dernier élément.

Dans votre deuxième exemple, vous commencez avec deux éléments, et la boucle foreach n'est pas au dernier élément, donc elle évalue le tableau à la prochaine itération 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 partie de l' itération de l'explication dans la documentation, ce qui signifie probablement que cela foreachfait toute la logique avant d'appeler 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 effectuée car elle a été modifiée "dans le temps". Mais si vous faites cela:

<?
    $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 qu'il se trouvait déjà au 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 n'a pas été bouclé.

Une explication détaillée peut être lue sur Comment fonctionne PHP «foreach»? ce qui explique les internes derrière ce comportement.

dkasipovic
la source
7
Eh bien, avez-vous lu le reste de la réponse? Il est parfaitement logique que foreach décide s'il va boucler une autre fois avant même d'y exécuter le code.
dkasipovic
2
Non, le tableau est modifié, mais "trop ​​tard" car foreach "pense" déjà qu'il est au dernier élément (qu'il est au début de l'itération) et ne bouclera plus. Où dans le deuxième exemple, il n'est pas au dernier élément au début de l'itération et réévalue au début de la prochaine itération. J'essaie de préparer un cas de test.
dkasipovic
1
@AlmaDo Regardez lxr.php.net/xref/PHP_TRUNK/Zend/zend_vm_def.h#4509 Il est toujours défini sur le pointeur suivant lors de son itération. Donc, quand il atteint la dernière itération, il sera marqué comme terminé (via le pointeur NULL). Lorsque vous ajoutez ensuite une clé lors de la dernière itération, foreach ne le remarquera pas.
bwoebi
1
@DKasipovic no. Il n'y a pas d' explication complète et claire là-bas (du moins pour l'instant - peut-être que je me trompe)
Alma Do
4
En fait, il semble que @AlmaDo ait un défaut dans la compréhension de sa propre logique… Votre réponse est très bien.
bwoebi
15

Selon la documentation fournie par le manuel PHP.

À chaque itération, la valeur de l'élément actuel est affectée à $ v et le
pointeur de tableau interne est avancé de un (donc à 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 avoir qu'un seul élément, donc selon l'exécution foreach, 1 assigner à $v et il n'a pas d'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);
}

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

user3535130
la source
13

Grande question, car de nombreux 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 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 produit:

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 une fois la boucle terminée. Cependant, 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
)

Les modifications par rapport à l'original ne peuvent pas être des avis, en fait il n'y a pas de changement par rapport à l'original, même si vous avez clairement attribué une valeur à $ item. En effet, vous travaillez sur $ item tel qu'il apparaît dans la copie de $ set en cours de traitement. Vous pouvez remplacer cela 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 que lorsque $ item est opéré par référence, les modifications apportées à $ item sont apportées aux membres de l'ensemble $ original. L'utilisation de $ item par référence 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 le montre l'exemple, PHP a copié $ set et l'a utilisé pour boucler, mais quand $ 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 chaque fois elle ajoute une autre valeur à la fin de l'ensemble $ d'origine, en laissant l'ensemble $ d'origine avec 6 éléments, mais sans entrer dans une boucle infinie.

Cependant, que se passerait-il si nous avions utilisé $ item par référence, comme je l'ai mentionné précédemment? 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, soit attendre que votre système d'exploitation manque de mémoire. J'ai ajouté la ligne suivante à mon script pour que PHP manque de mémoire très rapidement, je vous suggère de faire de même 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 boucle elle-même, le tableau reste statique pendant toute l'exécution de la boucle, vous ne rencontrerez donc jamais de problèmes.

Hrvoje Antunović
la source
7

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

Dans la boucle foreach, la première chose que fait php est qu'il crée une copie du tableau qui doit être itéré. PHP parcourt ensuite cette nouvelle copydu 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 plus de cela, php permet également 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: Il ne permet pas original array indexesd'être utilisé en tant que references.

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

Pranav Rana
la source
1
Object public variablesest faux ou au mieux trompeur. Vous ne pouvez pas utiliser un objet dans un tableau sans l'interface correcte (par exemple, Traversible) et lorsque vous le faites, foreach((array)$obj ...vous travaillez en fait avec un tableau simple, et non plus un objet.
Christian