Comment PHP 'foreach' fonctionne-t-il réellement?

Permettez-moi de préfixe en disant que je sais ce que foreach est, et comment l'utiliser. Cette question concerne comment cela fonctionne sous le capot, et je ne veux pas de réponses du genre "c'est comme ça qu'on boucle un tableau avec foreach ".


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

laissez-moi vous 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 des sources-autrement la boucle continuerait pour toujours, puisque 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 des sources 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 l'exécution, le pointeur interne du tableau est automatiquement réinitialisé au premier élément du tableau.

exact... cela semble suggérer que foreach dépend du pointeur du tableau source. Mais nous venons de prouver que nous sommes ne fonctionne pas avec le tableau des sources , droit? Eh bien, pas tout à fait.

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 montre ceci. Sauf que cela ne peut pas être vrai - si c'était le cas, alors test case 1 boucle pour toujours.

Le manuel PHP dit aussi:

comme foreach dépend de l'indicateur de tableau interne le changer dans la boucle peut conduire à 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 */

Test affaire 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 "copie de la source" théorie.


La Question

que se passe-t-il ici? Mon c-fu n'est pas assez bon pour que je puisse tirer 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 travaille avec un copie de la matrice, mais définit le pointeur sur le tableau de la source de tableau à la fin du tableau après la boucle.

  • est-ce correct et toute l'histoire?
  • si ce n'est pas le cas, que fait-il vraiment?
  • Est-il une situation où l'utilisation de fonctions qui permettent d'ajuster le pointeur sur le tableau ( each() , reset() et al.) pendant un foreach pourrait affecter le résultat de la boucle?
1683
demandé sur sergiol 2012-04-07 23:33:57
la source

7 ответов

foreach supporte l'itération sur trois différents types de valeurs:

dans ce qui suit, je vais essayer d'expliquer précisément comment fonctionne l'itération dans les différents cas. De loin le cas le plus simple sont Traversable des objets, comme pour ces foreach est essentiellement seulement la syntaxe de sucre pour 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éthodes réels sont évités en utilisant une API interne qui reflète essentiellement l'interface Iterator au niveau C.

L'itération des tableaux et des objets simples est beaucoup plus compliquée. Tout d'abord, il convient de noter que dans PHP "tableaux" sont réellement commandé dictionnaires et ils seront traversés selon cet ordre (qui correspond à la ordre d'insertion aussi longtemps que vous n'avez pas utilisé quelque chose comme sort ). Ceci est opposé à l'itération par l'ordre naturel des clés (comment les listes dans d'autres langues fonctionnent souvent) ou à l'absence d'ordre défini du tout (comment les dictionnaires dans d'autres langues fonctionnent souvent).

la même chose s'applique aussi aux objets, car les propriétés de l'objet peuvent être vues comme un autre dictionnaire (commandé) mettant en correspondance les noms des propriétés avec leurs valeurs, en plus de quelques manipulations de visibilité. Dans la majorité des cas, la les propriétés des objets ne sont pas stockées de cette manière plutôt inefficace. Cependant si vous commencez à itérer sur un objet, la représentation emballée qui est normalement utilisée sera convertie en un vrai dictionnaire. À ce moment-là, 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).

pour l'instant, tout va bien. Parcourir un dictionnaire ne peut pas être trop dur, non? Les problèmes commencent quand vous réalisez qu'un tableau/objet peut changer pendant l'itération. Il y a plusieurs façons d'y arriver:

  • si vous itérez par référence en utilisant foreach ($arr as &$v) alors $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 la valeur, mais le tableau était une référence avant: $ref =& $arr; foreach ($ref as $v)
  • objets ont by-handle passing sémantique, qui pour doit pratique signifie qu'ils se comportent comme des références. Ainsi, les objets peuvent toujours être changé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é. Disons que vous utilisez un pointeur pour garder une trace de l'élément de tableau où vous êtes actuellement. Si cet élément est maintenant libéré, il vous reste un pointeur pendulaire (résultant habituellement en une segfault).

il y a 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 vais décrire les deux comportements dans ce qui suit. Le résumé est que L'approche de PHP 5 était plutôt stupide et conduit à toutes sortes de bizarre bord-cas, tandis que L'approche de PHP 7 plus impliqué se traduit par un comportement plus prévisible et cohérent.

en dernier lieu, il est à noter que PHP utilise le comptage de référence et la copie-sur-écriture pour gérer la mémoire. Cela signifie que si vous "copier" une valeur, vous n'avez qu'à réutiliser l'ancienne valeur et incrémenter son compte de référence (refcount). Une fois que vous avez effectué une modification, une copie réelle (appelée "duplication") sera effectuée. Voir on vous ment à pour une introduction plus détaillée sur ce sujet.

PHP 5

Interne pointeur sur le tableau et HashPointer

tableaux en PHP 5 ont un pointeur de tableau interne dédié" (IAP), qui supporte correctement les modifications: chaque fois qu'un élément est supprimé, il y aura une vérification si L'IAP pointe vers cet élément. S'il le fait, 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 supporter deux boucles simultanées avec un seul pointeur de réseau interne, foreach exécute les schenanigans suivants: avant que le corps de boucle soit exécuté, foreach sauvegardera un pointeur vers l'élément courant et son hachage dans un per-foreach HashPointer . Après l'exécution du corps de boucle, L'IAP sera ramené à cet élément s'il existe toujours. Toutefois, si l'élément a été supprimé, nous allons simplement utiliser où que le PEI est actuellement. Ce schéma fonctionne en quelque sorte, mais il y a beaucoup de comportements bizarres que vous pouvez en retirer, dont certains que je vais vous montrer ci-dessous.

duplication de tableaux

L'IAP est une caractéristique visible d'un tableau (exposée à travers la famille de fonctions current ), car de telles modifications à L'IAP comptent comme des modifications sous sémantique copie-sur-écriture. Cela signifie malheureusement que foreach est dans de nombreux cas forcé 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, puis les changements à elle sont supposé à propager, donc il ne doit pas être dupliqué.
  2. le tableau a refcount>1. Si refcount est 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 refcount sera incrémenté (*). De plus, si foreach par référence est utilisé, alors le tableau (potentiellement dupliqué) sera tourné dans 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 éviter que les changements de Pia sur $arr ne fuient vers $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 malheureuse et un artefact de la mise en œuvre sous-optimale (il n'y a aucune préoccupation de modification pendant 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 de copie-sur-écriture (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 les valeurs de refcount=1. Cette violation entraîne un changement de comportement visible par l'utilisateur( alors que la vache est normalement transparente), parce que 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) de toujours dupliquer, b) de ne pas incrémenter le refcount et donc de permettre au tableau itéré d'être modifié arbitrairement dans la boucle, ou C) de ne pas utiliser l'IAP du tout (la solution PHP 7).

la Position de l'avancement de la commande

il y a un dernier détail de mise en œuvre que vous devez connaître pour bien comprendre les exemples de code ci-dessous. La façon "normale" de passer en boucle à travers une structure de données ressemblerait à quelque chose comme ceci en pseudo-code:

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

cependant foreach , étant un flocon de neige assez 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();
}

à savoir, le pointeur de tableau est déjà avancé avant le corps de boucle exécute. Cela signifie que pendant que le corps de 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 montrant une modification au cours de l'itération vont toujours désactiver l'é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 généralement complète des idiosyncrasies de la mise en œuvre de chaque foreach et nous pouvons passer à l'examen de quelques exemples.

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

  • dans les cas d'essai 1 et 2 $array commence avec refcount=1, Il ne sera donc pas dupliqué par foreach: seul le refcount est incrémenté. Lorsque le corps de boucle modifie par la suite 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 cas d'essai 3, encore une fois le tableau n'est pas dupliqué, donc foreach modifiera L'IAP de la variable $array . À la fin de l'itération, L'IAP est nul (ce qui signifie itération effectuée), ce qui signifie each indique en retournant false .

  • dans les cas d'essai 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 fonctionnera à nouveau sur un tableau séparé.

exemples: effets de current in foreach

une bonne façon de montrer les différents comportements de duplication est d'observer le comportement de la fonction current() à l'intérieur d'une boucle foreach. Prenons l'exemple suivant:

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

ici, vous devez savoir que current() est un sous-ref fonction (en fait: préférez-ref), même si elle ne modifie pas le tableau. Il faut pour jouer gentil avec toutes les autres fonctions comme le next , qui sont tous par-réf. Le passage de référence implique que le tableau doit être séparé et donc $array et le tableau de foreach sera différent. La raison pour laquelle vous obtenez 2 au lieu de 1 est également mentionnée ci-dessus: foreach avance le pointeur de 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 au second.

essayons maintenant une petite modification:

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

nous avons Ici l'is_ref=1 cas, le tableau n'est pas copié (comme ci-dessus). Mais maintenant qu'il s'agit d'une référence, le tableau ne doit plus être dupliqué en passant à la fonction by-ref current() . Ainsi current() et pour chaque travail sur le même tableau. Vous voir encore le comportement off-by-one cependant, en raison de la façon foreach avance le pointeur.

, Vous obtenez le même comportement quand on fait par-ref itération:

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 fondamentalement vous avez la même situation que ci-dessus.

une autre petite variation, cette fois nous assignerons le tableau à une autre variable:

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

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

exemples: Modification pendant l'itération

essayer pour tenir compte des modifications pendant l'itération est où tous nos problèmes foreach origine, 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 de 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 que (1, 2) est absent de la sortie, parce que l'élément 1 a été supprimé. Ce qui est probablement inattendu, c'est que l' boucle externe s'arrête après le premier élément. Pourquoi est-ce?

la raison derrière ceci est le hack de boucle imbriquée décrit ci-dessus: avant que le corps de boucle s'exécute, la position IAP actuelle et le hachage est sauvegardé dans un HashPointer . Après le corps de boucle il sera restauré, mais seulement si l'élément existe encore, 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é supprimer, il utilisera donc L'IAP, qui a déjà été marqué comme terminé par la boucle intérieure!

une autre conséquence du mécanisme HashPointer backup+restore est que les modifications à L'IAP bien que reset() etc. habituellement n'ont pas d'impact 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, tandis que reset() modifie temporairement le PEI, il sera restauré à l'élément courant avant chaque élément après le corps de boucle. Pour forcer reset() à faire un effet sur la boucle, vous devez en outre supprimer l'élément courant, 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 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: Hashes ont des collisions, et des pointeurs peuvent être réutilisés! Cela signifie que, avec un choix prudent de touches de tableau, nous pouvons faire foreach croire qu'un élément qui a été supprimé existe toujours, de sorte qu'il va sauter directement à elle. 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 se fait-il que 'FYFY' possède 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é, ce qui raccourcit la boucle.

remplaçant l'entité itérée pendant la boucle

un dernier cas étrange que je voudrais mentionner, est que PHP vous permet de remplacer l'entité itérée pendant la boucle. Vous pouvez donc commencer à itérer sur un tableau et le remplacer par un autre tableau à mi-parcours. Ou commencer à itérer sur un tableau puis le remplacer 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 va juste commencer à itérer l'autre entité dès le début une fois que la substitution a eu lieu.

PHP 7

table de hachage des itérateurs

si vous vous souvenez encore, le principal problème avec l'itération du tableau était comment gérer la suppression des éléments au milieu de l'itération. PHP 5 a utilisé un seul pointeur de tableau interne (IAP) à cette fin, ce qui était quelque peu sous-optimal, comme un tableau pointeur a dû être étiré pour soutenir plusieurs boucles simultanées foreach et interaction avec reset() etc. sur le dessus de cela.

PHP 7 utilise une approche différente, à savoir qu'il supporte la création d'une quantité arbitraire d'itérateurs externes et sûrs. 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 hashtable pointant vers celui élément sera avancé à l'élément suivant.

cela signifie que foreach n'utilisera plus le IAP du tout . La boucle foreach 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 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 du tableau des valeurs partielles 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 point une duplication se produira (selon copie-sur-écriture) et foreach continuera à travailler sur le vieux tableau.

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

$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 de valeurs partielles des tableaux de référence était des cas spéciaux. Dans ce cas, aucune duplication ne s'est produite, de sorte que toutes les modifications du tableau au cours de l'itération seraient reflétées par la boucle. En PHP 7, ce cas particulier est passé: une itération de by-value d'un tableau va toujours continuer à travailler sur les éléments originaux, sans tenir compte des modifications pendant la boucle.

cela, 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 répercutées par la boucle. Il est intéressant de noter qu'il en va de même pour l'itération en fonction de la 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 de la poignée des objets (c'est-à-dire qu'ils se comportent comme des références, même dans des contextes de valeurs partielles).

exemples

voyons quelques exemples, en commençant par votre test jurisprudence:

  • les cas D'essai 1 et 2 conservent le même résultat: l'itération du tableau des valeurs partielles continue toujours de travailler sur les éléments originaux. (Dans ce cas, même le refcounting et le comportement de duplication sont exactement les mêmes entre PHP 5 et PHP 7).

  • test case 3 Change: Foreach n'utilise plus L'IAP, de sorte que each() n'est pas affecté par la boucle. Il aura la même sortie avant et après.

  • les cas D'essai 4 et 5 restent les mêmes: each() et reset() dupliqueront le tableau avant de changer L'IAP, tandis que foreach utilise toujours le tableau original. (Non pas que le PEI changement aurait de l'importance, même si le tableau est partagé.)

la deuxième série d'exemples était liée au comportement de current() sous différentes configurations de référence/recomptage. Cela n'a plus de sens, comme current() n'est absolument pas affecté par la boucle, de sorte que sa valeur de retour reste toujours la même.

Cependant, nous obtenons quelques changements intéressants lorsque nous considérons des modifications au cours de l'itération. J'espère que vous trouverez le nouveau comportement plus sain d'esprit. 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 extérieure n'est plus interrompue après la première itération. La raison est que les deux boucles ont maintenant des itérateurs hashtable entièrement séparés, et il n'y a pas plus longue toute contamination croisée des deux boucles par un IAP partagé.

un autre cas de bord bizarre qui est fixé maintenant, est l'effet étrange que vous obtenez lorsque vous supprimez et ajoutez des éléments qui se produisent d'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 au nouvel élément, parce qu'il "ressemblait" à l'élément remove (dû à la collision de hachage et pointeur). Comme nous ne dépendons plus sur le hachage d'élément pour rien, ce n'est plus un problème.

1415
répondu NikiC 2018-08-07 12:55:29
la source

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

l'opérateur de tâche pour les tableaux en PHP fonctionne plus comme un clone paresseux. Attribuer une variable à une autre qui contient un tableau va cloner le tableau, contrairement à la plupart des langues. Cependant, le clonage ne sera effectué sauf si c'est nécessaire. Cela signifie que le clone n'aura lieu que lorsque l'une ou l'autre des variables est modifiée (copy-on-write).

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 de vivre seulement pendant la boucle et ensuite, ils sont tous les deux rejeté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 vivante. Ça déclenche un clone, et ça 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?

99
répondu linepogl 2014-04-15 15:10:06
la source

quelques points à noter en travaillant avec foreach() :

a) foreach travaille sur la prospecté "copie de 1519130920" du tableau original. Cela signifie que foreach () aura un stockage de données partagé jusqu'à ou à moins qu'un prospected copy ne soit non pas créé foreach Notes/commentaires de l'Utilisateur .

b) Qu'est-ce qui déclenche une copie recherchée ? Prospectés copie est créé sur la base de la politique de copy-on-write , c'est-à-dire quand un tableau passé à foreach() est modifié, un clone de l'original tableau est créé.

c) le tableau original et l'itérateur foreach() auront DISTINCT SENTINEL VARIABLES , c'est-à-dire un pour le tableau original et un autre pour foreach; voir le code d'essai ci-dessous. SPL , Itérateurs , et Tableau Itérateur .

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

l'exemple suivant montre que chacune des variables() et reset() n'affecte pas les variables SENTINEL . (for example, the current index variable) de la boucle 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/>";

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
37
répondu sakhunzai 2017-05-23 15:18:28
la source

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 " backward incompatible changes ", en PHP 7 travaux foreach sur la copie du tableau, de sorte que toute modification sur le tableau lui-même ne sont pas réfléchies sur la boucle foreach. Plus de détails sur le lien.

explication (citation de php.net ):

la première forme de boucle au-dessus du tableau donné par array_expression. Sur chaque itération, la valeur de l'élément courant est assignée à $valeur et le tableau interne pointeur est avancé par un (donc sur la prochaine itération, vous serez à la recherche à l'élément suivant).

Alors, 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'a pas existe, donc après que vous avez ajouté un nouvel élément pour chaque fin parce qu'il a déjà "décidé" qu'il comme le dernier élément.

dans votre second exemple, vous commencez avec deux éléments, et chaque boucle n'est pas au dernier élément, donc il évalue le tableau sur la prochaine itération et réalise ainsi qu'il y a un nouvel élément dans le tableau.

je crois que c'est toute la conséquence de sur chaque itération partie de l'explication dans le documentation, Ce qui signifie probablement que foreach fait 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'elle a accepté la modification et l'a faite parce qu'elle a été modifiée"à 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);
?>

Vous obtiendrez:

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

ce qui signifie que ce tableau a été modifié, mais puisque nous l'avons modifié quand le foreach était déjà au dernier élément du tableau, il a" décidé "de ne plus boucler la 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é.

explication détaillée peut être lu à Comment PHP 'foreach' fonctionne réellement? qui explique les intérieurs derrière ce comportement.

24
répondu Damir Kasipovic 2018-06-01 15:23:44
la source

selon la documentation fournie par PHP manual.

sur chaque itération, la valeur de l'élément courant est attribuée à $v et le
interne 1519110920" le pointeur de tableau est avancé par un (donc sur la prochaine itération, vous regarderez l'élément suivant).

comme dans votre premier exemple:

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

$array n'ont qu'un seul élément, de sorte que selon le foreach exécution, 1 affecter à $v et il n'y a aucun autre élément à déplacer le pointeur

mais dans votre second exemple:

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

$array ont deux éléments, donc maintenant $array évaluer les indices zéro et déplacer le pointeur par un. Pour la première itération de boucle, ajouté $array['baz']=3; comme passage par référence.

9
répondu user3535130 2014-04-15 13:35:09
la source

grande question, parce que 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 fait une copie du tableau qui est utilisé dans la boucle. La copie est jetée immédiatement après la fin de la boucle. C'est transparent dans le fonctionnement d'une simple boucle foreach. Par exemple:

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

Ce sorties:

apple
banana
coconut

ainsi la copie est créée mais le le développeur ne le remarque pas, parce que le tableau original n'est pas référencé dans la boucle ou après la fin de la boucle. Cependant, lorsque vous tentez de modifier les éléments dans une boucle, vous constatez qu'ils ne sont pas modifiés lorsque vous terminez:

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

print_r($set);

Ce sorties:

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

tous les changements par rapport à l'original ne peuvent pas être des avis, en fait il n'y a aucun changement par rapport à l'original, même si vous avez clairement assigné une valeur à $item. C'est parce que vous êtes fonctionnement sur $item tel qu'il apparaît sur la copie de $set en cours de travail. Vous pouvez outrepasser 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);

Ce sorties:

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

il est donc évident et observable, lorsque $item est opéré par référence, les changements faits à $item sont faits 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 ceci, nous allons d'abord montrer un script rapide démonstration de la copie:

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

Ce sorties:

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 boucler la boucle, mais quand $set était utilisé à l'intérieur de 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'assignation de $item. Pour cette raison, la boucle ci-dessus ne s'exécute que 3 fois, et à chaque fois elle s'ajoute une autre valeur à la fin de l'ensemble $original, laissant l'ensemble $original avec 6 éléments, mais n'entrant jamais dans une boucle infinie.

cependant, que se passerait-il si nous avions utilisé $item par référence, comme je l'ai déjà mentionné? Un caractère unique ajouté au test ci-dessus:

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

se traduit par 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 soit à court de mémoire. J'ai ajouté ce qui suit la ligne à mon script pour que PHP s'épuise 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. Quand une copie est créée et utilisée uniquement par la structure de la boucle elle-même, le tableau reste statique tout au long de l'exécution de la boucle, donc vous ne courrez jamais dans question.

6
répondu hrvojeA 2017-04-21 11:44:53
la source

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

dans chaque boucle, la première chose que php fait est de créer une copie du tableau qui doit être itérée. PHP itère ensuite sur ce nouveau copy du tableau plutôt que sur le tableau original. Ceci est démontré dans l'exemple suivant:

<?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 aussi. Cela 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

Note: Il ne permet pas de original array indexes pour être utilisé comme references .

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

4
répondu Pranav Rana 2017-11-13 17:08:45
la source

Autres questions sur php loops foreach iteration php-internals