PHP Foreach Pass by Reference: Duplication du dernier élément? (Punaise?)

159

J'ai juste eu un comportement très étrange avec un simple script php que j'écrivais. Je l'ai réduit au minimum nécessaire pour recréer le bogue:

<?php

$arr = array("foo",
             "bar",
             "baz");

foreach ($arr as &$item) { /* do nothing by reference */ }
print_r($arr);

foreach ($arr as $item) { /* do nothing by value */ }
print_r($arr); // $arr has changed....why?

?>

Cela produit:

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

Est-ce un bug ou un comportement vraiment étrange qui est censé se produire?

régalité
la source
Refaites-le par valeur, voyez si change la 3ème fois ...?
Shackrock
1
@Shackrock, cela ne semble plus changer avec la répétition des boucles par valeur.
regality
1
Fait intéressant, si vous modifiez la deuxième boucle pour utiliser autre chose que $ item, cela fonctionne comme prévu.
Steve Claridge
9
désactivez toujours l'élément à la fin du corps de la boucle: foreach($x AS &$y){ ... unset($y); }- il est en fait sur php.net (je ne sais pas où) parce que c'est une erreur bien commise.
Rudie
2
duplication possible du PHP Pass par référence dans foreach
Felix Kling

Réponses:

170

Après la première boucle foreach, il $itemy a toujours une référence à une valeur qui est également utilisée par $arr[2]. Ainsi, chaque appel foreach dans la deuxième boucle, qui n'appelle pas par référence, remplace cette valeur, et donc $arr[2], par la nouvelle valeur.

Donc boucle 1, la valeur et $arr[2]devenir $arr[0], qui est «toto».
Boucle 2, la valeur et $arr[2]devenir $arr[1], qui est «bar».
Boucle 3, la valeur et $arr[2]devenir $arr[2], qui est «bar» (à cause de la boucle 2).

La valeur «baz» est en fait perdue au premier appel de la deuxième boucle foreach.

Débogage de la sortie

Pour chaque itération de la boucle, nous allons faire écho à la valeur de $itemet imprimer récursivement le tableau $arr.

Lorsque la première boucle est exécutée, nous voyons cette sortie:

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

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

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

À la fin de la boucle, $itempointe toujours vers le même endroit que $arr[2].

Lorsque la deuxième boucle est exécutée, nous voyons cette sortie:

foo
Array ( [0] => foo [1] => bar [2] => foo )

bar
Array ( [0] => foo [1] => bar [2] => bar )

bar
Array ( [0] => foo [1] => bar [2] => bar )

Vous remarquerez que chaque fois que le tableau insère une nouvelle valeur $item, il est également mis $arr[3]à jour avec la même valeur, car ils pointent toujours vers le même emplacement. Lorsque la boucle atteint la troisième valeur du tableau, elle contiendra la valeur barcar elle vient d'être définie par l'itération précédente de cette boucle.

Est-ce un bug?

Non. Il s'agit du comportement d'un élément référencé et non d'un bogue. Ce serait similaire à exécuter quelque chose comme:

for ($i = 0; $i < count($arr); $i++) { $item = $arr[$i]; }

Une boucle foreach n'est pas de nature spéciale dans laquelle elle peut ignorer les éléments référencés. Il s'agit simplement de définir cette variable sur la nouvelle valeur à chaque fois, comme vous le feriez en dehors d'une boucle.

animuson
la source
4
J'ai une légère correction pédante. $itemn'est pas une référence à $arr[2], la valeur contenue par $arr[2]est une référence à la valeur référencée par $item. Pour illustrer la différence, vous pourriez également ne pas être défini $arr[2], et $itemne serait pas affecté, et écrire à $itemne l'affecterait pas.
Paul Biggar
2
Ce comportement est complexe à comprendre et peut entraîner des problèmes. Je garde cela comme l'un de mes favoris pour montrer à mes élèves pourquoi ils devraient éviter (aussi longtemps qu'ils le peuvent) les choses "par référence".
Olivier Pons
1
Pourquoi ne $itemsort-il pas de la portée lorsque la boucle foreach est sortie? Cela semble être un problème de fermeture?
jocull
6
@jocull: EN PHP, foreach, for, while, etc. ne créent pas leur propre portée.
animuson
1
@jocull, PHP n'a pas (de bloc) de variables locales. Une des raisons pour lesquelles cela m'ennuie.
Qtax
29

$itemest une référence à $arr[2]et est écrasée par la deuxième boucle foreach comme l'a souligné animuson.

foreach ($arr as &$item) { /* do nothing by reference */ }
print_r($arr);

unset($item); // This will fix the issue.

foreach ($arr as $item) { /* do nothing by value */ }
print_r($arr); // $arr has changed....why?
Michael Leaney
la source
3

Bien que ce ne soit pas officiellement un bogue, à mon avis, c'est le cas. Je pense que le problème ici est que nous nous attendons à ce que nous devenions $itemhors de portée lorsque la boucle est sortie, comme dans de nombreux autres langages de programmation. Cependant, cela ne semble pas être le cas ...

Ce code ...

$arr = array('one', 'two', 'three');
foreach($arr as $item){
    echo "$item\n";
}    
echo $item;

Donne la sortie ...

one
two
three
three

Comme d'autres l'ont déjà dit, vous écrasez la variable référencée $arr[2]avec votre deuxième boucle, mais cela ne se produit que parce qu'il $itemn'est jamais sorti hors de portée. Que pensez-vous les gars ... bug?

plaisanter
la source
4
1) Pas un bug. Il est déjà mentionné dans le manuel et rejeté dans un certain nombre de rapports de bogues comme prévu. 2) Ne répond pas vraiment à la question ...
BoltClock
Cela m'a surpris non pas à cause du problème de portée, je m'attendais à ce que $ item reste après le foreach initial, mais je ne me suis pas rendu compte que foreach MISE À JOUR la variable au lieu de la REMPLACER. par exemple, la même chose que d'exécuter unset ($ item) avant la deuxième boucle. Notez que unset n'efface pas la valeur (et donc le dernier élément du tableau), il supprime simplement la variable.
Programmeur
Malheureusement, PHP ne crée pas de nouvelle portée pour les boucles ou les {}blocs en général. Voici comment fonctionne la langue
Fabian Schmengler
0

Le comportement correct de PHP pourrait être une erreur de NOTICE à mon avis. Si une variable référencée créée dans une boucle foreach est utilisée en dehors de la boucle, cela devrait provoquer une notification. Très facile de tomber dans ce comportement, très difficile de le repérer quand il s'est produit. Et aucun développeur ne lira la page de documentation foreach, ce n'est pas une aide.

Vous devriez unset()la référence après votre boucle pour éviter ce genre de problème. unset () sur une référence supprimera simplement la référence sans nuire aux données d'origine.

John
la source
0

c'est parce que vous utilisez par la directive ref (&). la dernière valeur sera remplacée par la deuxième boucle et cela corrompra votre tableau. la solution la plus simple consiste à utiliser un nom différent pour la deuxième boucle:

foreach ($arr as &$item) { ... }

foreach ($arr as $anotherItem) { ... }
Amir Surnay
la source