PHP Foreach Pass por referencia: ¿Duplicación del último elemento? (¿Insecto?)

159

Acabo de tener un comportamiento muy extraño con un simple script php que estaba escribiendo. Lo reduje al mínimo necesario para recrear el error:

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

?>

Esto produce:

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

¿Es esto un error o un comportamiento realmente extraño que se supone que debe suceder?

privilegios reales
fuente
Hazlo de nuevo por valor, mira si cambia la tercera vez ...?
Shackrock
1
@Shackrock, ya no parece cambiar con la repetición de bucles por valor.
regality
1
Curiosamente, si cambia el segundo ciclo para usar algo que no sea $ item, entonces funciona como se esperaba.
Steve Claridge
9
siempre desarme el elemento al final del cuerpo del bucle: foreach($x AS &$y){ ... unset($y); }en realidad está en php.net (no sé dónde) porque es un error muy cometido.
Rudie
2
posible duplicado de PHP Pass por referencia en foreach
Felix Kling

Respuestas:

170

Después del primer bucle foreach, $itemsigue siendo una referencia a algún valor que también está siendo utilizado por $arr[2]. Por lo tanto, cada llamada foreach en el segundo bucle, que no llama por referencia, reemplaza ese valor y $arr[2], por lo tanto , con el nuevo valor.

Entonces, el bucle 1, el valor y el $arr[2]devenir $arr[0], que es 'foo'.
Loop 2, el valor y $arr[2]llegar a ser $arr[1], que es 'bar'.
Bucle 3, el valor y $arr[2]llegar a ser $arr[2], que es 'bar' (debido al bucle 2).

El valor 'baz' en realidad se pierde en la primera llamada del segundo bucle foreach.

Depuración de la salida

Para cada iteración del bucle, haremos eco del valor $iteme imprimiremos recursivamente la matriz $arr.

Cuando se ejecuta el primer ciclo, vemos esta salida:

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

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

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

Al final del bucle, $itemsigue apuntando al mismo lugar que $arr[2].

Cuando se ejecuta el segundo ciclo, vemos esta salida:

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

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

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

Notarás cómo cada matriz de tiempo pone un nuevo valor $item, también se actualiza $arr[3]con ese mismo valor, ya que ambos todavía apuntan a la misma ubicación. Cuando el bucle llega al tercer valor de la matriz, contendrá el valor barporque solo fue establecido por la iteración anterior de ese bucle.

¿Es un error?

No. Este es el comportamiento de un elemento referenciado, y no un error. Sería similar a ejecutar algo como:

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

Un bucle foreach no es de naturaleza especial en el que puede ignorar los elementos referenciados. Simplemente establece esa variable en el nuevo valor cada vez como lo haría fuera de un ciclo.

animuson
fuente
44
Tengo una ligera corrección pedante. $itemno es una referencia a $arr[2], el valor contenido por $arr[2]es una referencia al valor al que hace referencia $item. Para ilustrar la diferencia, también podría desarmar $arr[2], y no $itemse vería afectado, y escribir para $itemno afectarlo.
Paul Biggar
2
Este comportamiento es complejo de entender y puede generar problemas. Mantengo esto como uno de mis favoritos para mostrar a mis alumnos por qué deberían evitar (siempre que puedan) las cosas "por referencia".
Olivier Pons
1
¿Por qué $itemno sale del alcance cuando se sale del bucle foreach? Esto parece un problema de cierre?
Jocull
66
@jocull: EN PHP, foreach, for, while, etc. no crean su propio alcance.
animuson
1
@jocull, PHP no tiene (bloque) variables locales. Una de las razones por las que me molesta.
Qtax
29

$itemes una referencia $arr[2]y está siendo sobrescrito por el segundo bucle foreach como lo señaló 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
fuente
3

Si bien esto puede no ser oficialmente un error, en mi opinión lo es. Creo que el problema aquí es que tenemos la expectativa de $itemsalir del alcance cuando se cierra el ciclo como lo haría en muchos otros lenguajes de programación. Sin embargo, ese no parece ser el caso ...

Este código ...

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

Da la salida ...

one
two
three
three

Como ya dijeron otras personas, está sobrescribiendo la variable referenciada $arr[2]con su segundo bucle, pero solo sucede porque $itemnunca salió del alcance. ¿Qué piensan ustedes ... error?

jocull
fuente
44
1) No es un error. Ya se mencionó en el manual y se descartó en varios informes de errores según lo previsto. 2) Realmente no responde la pregunta ...
BoltClock
No me llamó la atención por el problema del alcance, esperaba que $ item permaneciera después del foreach inicial, pero no me di cuenta de que foreach ACTUALIZA la variable en lugar de REEMPLAZARLA. por ejemplo, lo mismo que ejecutar unset ($ item) antes del segundo ciclo. Tenga en cuenta que unset no borra el valor (y, por lo tanto, el último elemento de la matriz), simplemente elimina la variable.
Programador
Desafortunadamente, PHP no crea un nuevo alcance para bucles o {}bloques en general. Así es como funciona el lenguaje
Fabian Schmengler
0

El comportamiento correcto de PHP podría ser un error de AVISO en mi opinión. Si una variable referenciada creada en un bucle foreach se usa fuera del bucle, debería generar un aviso. Muy fácil caer en este comportamiento, muy difícil detectarlo cuando sucedió. Y ningún desarrollador va a leer la página de documentación foreach, no es una ayuda.

Debe hacer unset()referencia después de su ciclo para evitar este tipo de problema. unset () en una referencia solo eliminará la referencia sin dañar los datos originales.

Juan
fuente
0

eso es porque lo usas por la directiva ref (&). el último valor será reemplazado por el segundo bucle y corromperá su matriz. La solución más simple es usar un nombre diferente para el segundo bucle:

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

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