¿Cómo funciona realmente PHP 'foreach'?

2019

Permítanme ponerle un prefijo diciendo que sé qué foreaches, qué hace y cómo usarlo. Esta pregunta se refiere a cómo funciona bajo el capó, y no quiero ninguna respuesta en la línea de "así es como se repite una matriz con foreach".


Durante mucho tiempo supuse que foreachfuncionaba con la matriz en sí. Luego encontré muchas referencias al hecho de que funciona con una copia de la matriz, y desde entonces he asumido que este es el final de la historia. Pero recientemente tuve una discusión sobre el asunto, y después de un poco de experimentación descubrí que esto no era 100% cierto.

Déjame mostrarte lo que quiero decir. Para los siguientes casos de prueba, trabajaremos con la siguiente matriz:

$array = array(1, 2, 3, 4, 5);

Caso de prueba 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 */

Esto muestra claramente que no estamos trabajando directamente con la matriz fuente; de ​​lo contrario, el ciclo continuaría para siempre, ya que constantemente estamos empujando elementos a la matriz durante el ciclo. Pero solo para estar seguro de que este es el caso:

Caso de prueba 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 */

Esto respalda nuestra conclusión inicial, estamos trabajando con una copia de la matriz fuente durante el ciclo, de lo contrario veríamos los valores modificados durante el ciclo. Pero...

Si miramos en el manual , encontramos esta declaración:

Cuando foreach comienza a ejecutarse por primera vez, el puntero interno de la matriz se restablece automáticamente al primer elemento de la matriz.

Correcto ... esto parece sugerir que se foreachbasa en el puntero de matriz de la matriz fuente. Pero acabamos de demostrar que no estamos trabajando con la matriz fuente , ¿verdad? Bueno, no del todo.

Caso de prueba 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)
*/

Entonces, a pesar del hecho de que no estamos trabajando directamente con la matriz fuente, estamos trabajando directamente con el puntero de la matriz fuente; el hecho de que el puntero esté al final de la matriz al final del ciclo lo demuestra. Excepto que esto no puede ser cierto; si lo fuera, el caso de prueba 1 se repetiría para siempre.

El manual de PHP también establece:

Como foreach se basa en el puntero interno de la matriz, cambiarlo dentro del ciclo puede conducir a un comportamiento inesperado.

Bueno, descubramos qué es ese "comportamiento inesperado" (técnicamente, cualquier comportamiento es inesperado ya que ya no sé qué esperar).

Caso de prueba 4 :

foreach ($array as $key => $item) {
  echo "$item\n";
  each($array);
}

/* Output: 1 2 3 4 5 */

Caso de prueba 5 :

foreach ($array as $key => $item) {
  echo "$item\n";
  reset($array);
}

/* Output: 1 2 3 4 5 */

... nada tan inesperado allí, de hecho parece apoyar la teoría de la "copia de la fuente".


La pregunta

¿Que esta pasando aqui? Mi C-fu no es lo suficientemente bueno para poder extraer una conclusión adecuada simplemente mirando el código fuente de PHP, agradecería que alguien pudiera traducirlo al inglés por mí.

Me parece que foreachfunciona con una copia de la matriz, pero establece el puntero de la matriz de origen al final de la matriz después del bucle.

  • ¿Es esto correcto y toda la historia?
  • Si no, ¿qué está haciendo realmente?
  • ¿Existe alguna situación en la que el uso de funciones que ajustan el puntero de la matriz ( each(), reset()et al.) Durante un proceso foreachpueda afectar el resultado del ciclo?
DaveRandom
fuente
55
@DaveRandom Hay una etiqueta php-internals con la que probablemente debería ir, pero te dejaré a ti decidir cuál de las otras 5 etiquetas reemplazar.
Michael Berkowski el
55
se parece a la VACA, sin el identificador de eliminación
zb '
149
Al principio pensé »Dios, otra pregunta para novatos. Lea los documentos ... hm, comportamiento claramente indefinido «. Luego leí la pregunta completa y debo decir: me gusta. Te has esforzado bastante y has escrito todos los casos de prueba. PD. ¿son iguales los testcase 4 y 5?
knittl
21
Solo una idea de por qué tiene sentido que se toque el puntero de la matriz: PHP necesita reiniciar y mover el puntero interno de la matriz original junto con la copia, porque el usuario puede pedir una referencia al valor actual ( foreach ($array as &$value)) - PHP necesita saber la posición actual en la matriz original a pesar de que en realidad está iterando sobre una copia.
Niko
44
@Sean: En mi humilde opinión, la documentación de PHP es realmente bastante mala para describir los matices de las características principales del lenguaje. Pero eso es, tal vez, porque muchos casos especiales ad-hoc están incluidos en el idioma ...
Oliver Charlesworth

Respuestas:

1660

foreach admite iteración sobre tres tipos diferentes de valores:

A continuación, intentaré explicar con precisión cómo funciona la iteración en diferentes casos. Con mucho, el caso más simple son los Traversableobjetos, ya que para estos foreaches esencialmente solo el azúcar de sintaxis para el código a lo largo de estas líneas:

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();
    /* ... */
}

Para las clases internas, las llamadas a métodos reales se evitan mediante el uso de una API interna que esencialmente solo refleja la Iteratorinterfaz en el nivel C.

La iteración de matrices y objetos simples es significativamente más complicada. En primer lugar, debe tenerse en cuenta que en PHP las "matrices" son diccionarios realmente ordenados y se recorrerán de acuerdo con este orden (que coincide con el orden de inserción siempre y cuando no haya utilizado algo así sort). Esto se opone a iterar por el orden natural de las teclas (cómo funcionan a menudo las listas en otros idiomas) o no tener un orden definido en absoluto (cómo funcionan a menudo los diccionarios en otros idiomas).

Lo mismo también se aplica a los objetos, ya que las propiedades de los objetos se pueden ver como otros nombres de propiedades de mapeo de diccionario (ordenados) a sus valores, más un poco de manejo de visibilidad. En la mayoría de los casos, las propiedades del objeto no se almacenan de esta manera bastante ineficiente. Sin embargo, si comienza a iterar sobre un objeto, la representación empaquetada que se usa normalmente se convertirá en un diccionario real. En ese punto, la iteración de objetos simples se vuelve muy similar a la iteración de matrices (por lo que no estoy discutiendo mucho sobre la iteración de objetos simples aquí).

Hasta aquí todo bien. Iterar sobre un diccionario no puede ser demasiado difícil, ¿verdad? Los problemas comienzan cuando te das cuenta de que una matriz / objeto puede cambiar durante la iteración. Hay varias formas en que esto puede suceder:

  • Si itera por referencia usando, foreach ($arr as &$v)entonces $arrse convierte en una referencia y puede cambiarlo durante la iteración.
  • En PHP 5, lo mismo se aplica incluso si itera por valor, pero la matriz era una referencia de antemano: $ref =& $arr; foreach ($ref as $v)
  • Los objetos tienen una semántica pasajera, que para la mayoría de los propósitos prácticos significa que se comportan como referencias. Por lo tanto, los objetos siempre se pueden cambiar durante la iteración.

El problema con permitir modificaciones durante la iteración es el caso en el que se elimina el elemento en el que se encuentra actualmente. Supongamos que usa un puntero para realizar un seguimiento del elemento de matriz en el que se encuentra actualmente. Si este elemento ahora está liberado, le queda un puntero colgante (que generalmente resulta en una falla predeterminada).

Hay diferentes formas de resolver este problema. PHP 5 y PHP 7 difieren significativamente en este aspecto y describiré ambos comportamientos a continuación. El resumen es que el enfoque de PHP 5 fue bastante tonto y condujo a todo tipo de problemas extraños, mientras que el enfoque más complicado de PHP 7 da como resultado un comportamiento más predecible y consistente.

Como último aspecto preliminar, debe tenerse en cuenta que PHP utiliza el recuento de referencias y la copia en escritura para administrar la memoria. Esto significa que si "copia" un valor, en realidad solo reutiliza el valor anterior e incrementa su recuento de referencia (recuento). Solo una vez que realice algún tipo de modificación, se realizará una copia real (llamada "duplicación"). Vea Le están mintiendo para una introducción más extensa sobre este tema.

PHP 5

Puntero de matriz interna y HashPointer

Las matrices en PHP 5 tienen un "puntero de matriz interno" (IAP) dedicado, que admite modificaciones correctamente: cada vez que se elimina un elemento, se comprobará si el IAP apunta a este elemento. Si lo hace, se avanza al siguiente elemento en su lugar.

Si bien foreachhace uso del IAP, hay una complicación adicional: solo hay un IAP, pero una matriz puede ser parte de múltiples foreachbucles:

// 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) {
        // ...
    }
}

Para admitir dos bucles simultáneos con un solo puntero de matriz interno, foreachrealiza las siguientes travesuras: antes de ejecutar el cuerpo del bucle, realizará foreachuna copia de seguridad del puntero al elemento actual y su hash en un foreach HashPointer. Después de que se ejecute el cuerpo del bucle, el IAP se restablecerá en este elemento si aún existe. Sin embargo, si el elemento se ha eliminado, solo lo usaremos donde sea que esté actualmente el IAP. Este esquema funciona principalmente, pero hay un montón de comportamientos extraños que puedes obtener, algunos de los cuales demostraré a continuación.

Duplicación de matriz

El IAP es una característica visible de una matriz (expuesta a través de la currentfamilia de funciones), ya que tales cambios en el IAP cuentan como modificaciones bajo la semántica de copiar en escritura. Esto, desafortunadamente, significa que foreachen muchos casos se ve obligado a duplicar la matriz sobre la que está iterando. Las condiciones precisas son:

  1. La matriz no es una referencia (is_ref = 0). Si se trata de una referencia, se supone que los cambios se propaguen, por lo que no debe duplicarse.
  2. La matriz tiene refcount> 1. Si refcountes 1, la matriz no se comparte y podemos modificarla directamente.

Si la matriz no está duplicada (is_ref = 0, refcount = 1), solo refcountse incrementará (*). Además, si foreachse usa por referencia, la matriz (potencialmente duplicada) se convertirá en una referencia.

Considere este código como un ejemplo donde se produce la duplicación:

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

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

Aquí, $arrse duplicará para evitar que los cambios de IAP se $arrfiltren $outerArr. En términos de las condiciones anteriores, la matriz no es una referencia (is_ref = 0) y se usa en dos lugares (refcount = 2). Este requisito es desafortunado y un artefacto de la implementación subóptima (no hay ninguna preocupación de modificación durante la iteración aquí, por lo que realmente no necesitamos usar el IAP en primer lugar).

(*) Incrementar el refcountaquí suena inocuo, pero viola la semántica de copia en escritura (COW): Esto significa que vamos a modificar el IAP de una matriz refcount = 2, mientras que COW dicta que las modificaciones solo se pueden realizar en refcount = 1 valores. Esta violación da como resultado un cambio de comportamiento visible para el usuario (mientras que un COW es normalmente transparente) porque el cambio de IAP en la matriz iterada será observable, pero solo hasta la primera modificación no IAP en la matriz. En cambio, las tres opciones "válidas" hubieran sido a) duplicar siempre, b) no incrementen refcounty, por lo tanto, permitan que la matriz iterada se modifique arbitrariamente en el bucle o c) no utilicen el IAP (el PHP 7 solución).

Orden de avance de posición

Hay un último detalle de implementación que debe tener en cuenta para comprender correctamente los ejemplos de código a continuación. La forma "normal" de recorrer alguna estructura de datos se vería así en pseudocódigo:

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

Sin embargo foreach, al ser un copo de nieve bastante especial, elige hacer las cosas de manera ligeramente diferente:

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

A saber, el puntero de la matriz ya se mueve hacia adelante antes de que se ejecute el cuerpo del bucle. Esto significa que mientras el cuerpo del bucle está trabajando en el elemento $i, el IAP ya está en el elemento $i+1. Esta es la razón por la cual las muestras de código que muestran modificaciones durante la iteración siempre serán unsetel siguiente elemento, en lugar del actual.

Ejemplos: sus casos de prueba

Los tres aspectos descritos anteriormente deberían proporcionarle una impresión mayormente completa de las idiosincrasias de la foreachimplementación y podemos pasar a discutir algunos ejemplos.

El comportamiento de sus casos de prueba es simple de explicar en este punto:

  • En los casos de prueba 1 y 2 $arraycomienza con refcount = 1, por lo que no se duplicará por foreach: Solo refcountse incrementa el. Cuando el cuerpo del bucle posteriormente modifica la matriz (que tiene refcount = 2 en ese punto), la duplicación ocurrirá en ese punto. Foreach continuará trabajando en una copia no modificada de $array.

  • En el caso de prueba 3, una vez más la matriz no está duplicada, por foreachlo tanto , se modificará el IAP de la $arrayvariable. Al final de la iteración, el IAP es NULL (lo que significa que la iteración ha terminado), lo que eachindica al regresar false.

  • En los casos de prueba 4 y 5 tanto eachy resetson funciones por referencia. El $arraytiene una refcount=2cuando se pasa a ellos, así que tiene que ser duplicado. Como tal foreach, trabajará en una matriz separada nuevamente.

Ejemplos: efectos de currenten foreach

Una buena manera de mostrar los diversos comportamientos de duplicación es observar el comportamiento de la current()función dentro de un foreachbucle. Considere este ejemplo:

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

Aquí debe saber que current()es una función by-ref (en realidad: prefer-ref), a pesar de que no modifica la matriz. Tiene que ser para jugar bien con todas las otras funciones como las nextque son todas por ref. By-referencia de pasada implica que la matriz tiene que ser separado y por lo tanto $arrayy el foreach-arrayserá diferente. La razón que obtienes en 2lugar de 1también se menciona anteriormente: foreachavanza el puntero de matriz antes de ejecutar el código de usuario, no después. Entonces, aunque el código está en el primer elemento, foreachya avanzó el puntero al segundo.

Ahora intentemos una pequeña modificación:

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

Aquí tenemos el caso is_ref = 1, por lo que la matriz no se copia (al igual que arriba). Pero ahora que es una referencia, la matriz ya no tiene que duplicarse al pasar a la función by-ref current(). Así current()y foreachtrabajar en la misma matriz. Sin embargo, todavía ve el comportamiento off-by-one, debido a la forma en que foreachavanza el puntero.

Obtiene el mismo comportamiento cuando realiza la iteración by-ref:

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

Aquí la parte importante es que foreach creará $arrayun is_ref = 1 cuando se itera por referencia, por lo que básicamente tiene la misma situación que la anterior.

Otra pequeña variación, esta vez asignaremos la matriz a otra variable:

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

Aquí el recuento de la $arrayes 2 cuando se inicia el ciclo, por lo que por una vez tenemos que hacer la duplicación por adelantado. Por lo tanto, $arrayy la matriz utilizada por foreach estará completamente separada desde el principio. Es por eso que obtiene la posición del IAP donde estaba antes del bucle (en este caso, estaba en la primera posición).

Ejemplos: modificación durante la iteración

Intentar dar cuenta de las modificaciones durante la iteración es donde se originaron todos nuestros problemas foreach, por lo que sirve considerar algunos ejemplos para este caso.

Considere estos bucles anidados sobre la misma matriz (donde se usa la iteración por referencia para asegurarse de que realmente sea la misma):

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 parte esperada aquí es que (1, 2)falta en la salida porque 1se eliminó el elemento . Lo que probablemente sea inesperado es que el bucle externo se detiene después del primer elemento. ¿Porqué es eso?

La razón detrás de esto es el hack de bucle anidado descrito anteriormente: antes de que se ejecute el cuerpo del bucle, la posición IAP actual y el hash se respaldan en a HashPointer. Después del cuerpo del bucle, se restaurará, pero solo si el elemento aún existe, de lo contrario, se usa la posición IAP actual (cualquiera que sea). En el ejemplo anterior, este es exactamente el caso: el elemento actual del bucle externo se ha eliminado, por lo que utilizará el IAP, que ya ha sido marcado como terminado por el bucle interno.

Otra consecuencia del HashPointermecanismo de copia de seguridad + restauración es que los cambios en el IAP a través de reset()etc. generalmente no afectan foreach. Por ejemplo, el siguiente código se ejecuta como si reset()no estuviera presente:

$array = [1, 2, 3, 4, 5];
foreach ($array as &$value) {
    var_dump($value);
    reset($array);
}
// output: 1, 2, 3, 4, 5

La razón es que, si bien reset()modifica temporalmente el IAP, se restaurará al elemento foreach actual después del cuerpo del bucle. Para forzar reset()un efecto en el bucle, debe eliminar adicionalmente el elemento actual, de modo que el mecanismo de copia de seguridad / restauración falle:

$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

Pero, esos ejemplos aún son sanos. La verdadera diversión comienza si recuerda que la HashPointerrestauración usa un puntero al elemento y su hash para determinar si todavía existe. Pero: los hashes tienen colisiones, y los punteros se pueden reutilizar. Esto significa que, con una elección cuidadosa de las claves de la matriz, podemos hacer foreachcreer que todavía existe un elemento que se ha eliminado, por lo que saltará directamente a él. Un ejemplo:

$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

Aquí normalmente deberíamos esperar la salida de 1, 1, 3, 4acuerdo con las reglas anteriores. Lo que sucede es que 'FYFY'tiene el mismo hash que el elemento eliminado 'EzFY', y el asignador reutiliza la misma ubicación de memoria para almacenar el elemento. Entonces foreach termina saltando directamente al elemento recién insertado, acortando así el bucle.

Sustituyendo la entidad iterada durante el ciclo

Un último caso extraño que me gustaría mencionar, es que PHP le permite sustituir la entidad iterada durante el ciclo. Entonces puede comenzar a iterar en una matriz y luego reemplazarla con otra matriz a mitad de camino. O comience a iterar en una matriz y luego reemplácela con un objeto:

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

Como puede ver en este caso, PHP solo comenzará a iterar la otra entidad desde el principio una vez que haya ocurrido la sustitución.

PHP 7

Iteradores de tabla hash

Si aún recuerda, el principal problema con la iteración de matriz era cómo manejar la eliminación de elementos a mitad de la iteración. PHP 5 usó un único puntero de matriz interna (IAP) para este propósito, que era algo subóptimo, ya que un puntero de matriz tenía que estirarse para soportar múltiples bucles foreach simultáneos e interacción con reset()etc., además de eso.

PHP 7 utiliza un enfoque diferente, es decir, admite la creación de una cantidad arbitraria de iteradores de tabla hash seguros y externos. Estos iteradores deben registrarse en la matriz, a partir de ese punto tienen la misma semántica que el IAP: si se elimina un elemento de la matriz, todos los iteradores de tabla hash que apuntan a ese elemento avanzarán al siguiente elemento.

Esto significa que foreachya no se vayan a utilizar el IAP en absoluto . El foreachciclo no tendrá ningún efecto en los resultados de current()etc. y su propio comportamiento nunca estará influenciado por funciones como reset()etc.

Duplicación de matriz

Otro cambio importante entre PHP 5 y PHP 7 se relaciona con la duplicación de matrices. Ahora que el IAP ya no se usa, la iteración de matriz por valor solo hará un refcountincremento (en lugar de duplicar la matriz) en todos los casos. Si la matriz se modifica durante el foreachciclo, en ese momento se producirá una duplicación (de acuerdo con la copia en escritura) y foreachseguirá trabajando en la matriz anterior.

En la mayoría de los casos, este cambio es transparente y no tiene otro efecto que un mejor rendimiento. Sin embargo, hay una ocasión en la que da como resultado un comportamiento diferente, a saber, el caso en el que la matriz era una referencia previa:

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

Anteriormente, la iteración por valor de las matrices de referencia era casos especiales. En este caso, no se produjo duplicación, por lo que todas las modificaciones de la matriz durante la iteración se reflejarían en el bucle. En PHP 7, este caso especial desapareció: una iteración por valor de una matriz siempre seguirá trabajando en los elementos originales, sin tener en cuenta las modificaciones durante el ciclo.

Esto, por supuesto, no se aplica a la iteración por referencia. Si itera por referencia, todas las modificaciones se reflejarán en el bucle. Curiosamente, lo mismo es cierto para la iteración por valor de objetos 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 */

Esto refleja la semántica de los objetos (por ejemplo, se comportan como referencias incluso en contextos de valor).

Ejemplos

Consideremos algunos ejemplos, comenzando con sus casos de prueba:

  • Los casos de prueba 1 y 2 conservan el mismo resultado: la iteración de matriz de valores siempre funciona en los elementos originales. (En este caso, el refcountingcomportamiento uniforme y de duplicación es exactamente el mismo entre PHP 5 y PHP 7).

  • Cambios en el caso de prueba 3: Foreachya no usa el IAP, por each()lo que no se ve afectado por el bucle. Tendrá la misma salida antes y después.

  • Los casos de prueba 4 y 5 permanecen igual: each()y reset()duplicarán la matriz antes de cambiar el IAP, mientras que foreachtodavía usa la matriz original. (No es que el cambio de IAP hubiera importado, incluso si se hubiera compartido la matriz).

El segundo conjunto de ejemplos estaba relacionado con el comportamiento de current()diferentes reference/refcountingconfiguraciones. Esto ya no tiene sentido, ya que current()el ciclo no lo afecta por completo, por lo que su valor de retorno siempre permanece igual.

Sin embargo, obtenemos algunos cambios interesantes cuando consideramos modificaciones durante la iteración. Espero que encuentres el nuevo comportamiento más sano. El primer ejemplo:

$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) 

Como puede ver, el bucle externo ya no se cancela después de la primera iteración. La razón es que ambos bucles ahora tienen iteradores de tabla hash completamente separados, y ya no hay contaminación cruzada de ambos bucles a través de un IAP compartido.

Otro caso de borde extraño que se soluciona ahora, es el efecto extraño que obtienes al eliminar y agregar elementos que tienen el mismo hash:

$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

Anteriormente, el mecanismo de restauración de HashPointer saltaba directamente al nuevo elemento porque "parecía" que era lo mismo que el elemento eliminado (debido al hash y al puntero colisionantes). Como ya no confiamos en el elemento hash para nada, esto ya no es un problema.

NikiC
fuente
44
@Baba lo hace. Pasarlo a una función es lo mismo que hacer $foo = $arrayantes del ciclo;)
NikiC
32
Para aquellos de ustedes que no saben qué es un zval, consulte el blog de
shu zOMG chen
1
Corrección menor: lo que llamas Bucket no es lo que normalmente se llama Bucket en una tabla hash. Normalmente, Bucket es un conjunto de entradas con el mismo tamaño de hash%. Parece que lo usa para lo que normalmente se llama una entrada. La lista vinculada no está en cubos, sino en entradas.
unbeli
12
@unbeli Estoy usando la terminología utilizada internamente por PHP. Los Buckets son parte de una lista doblemente vinculada para colisiones de hash y también parte de una lista doblemente vinculada para el orden;)
NikiC
44
Gran respuesta. Creo que quisiste decir iterate($outerArr);y no en iterate($arr);alguna parte.
niahoo
116

En el ejemplo 3 no modificas la matriz. En todos los demás ejemplos, modifica el contenido o el puntero interno de la matriz. Esto es importante cuando se trata de matrices PHP debido a la semántica del operador de asignación.

El operador de asignación para las matrices en PHP funciona más como un clon perezoso. Asignar una variable a otra que contenga una matriz clonará la matriz, a diferencia de la mayoría de los idiomas. Sin embargo, la clonación real no se realizará a menos que sea necesaria. Esto significa que el clon tendrá lugar solo cuando cualquiera de las variables se modifique (copiar en escritura).

Aquí hay un ejemplo:

$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.

Volviendo a sus casos de prueba, puede imaginar fácilmente que foreachcrea algún tipo de iterador con una referencia a la matriz. Esta referencia funciona exactamente como la variable $ben mi ejemplo. Sin embargo, el iterador junto con la referencia viven solo durante el ciclo y luego, ambos se descartan. Ahora puede ver que, en todos los casos excepto 3, la matriz se modifica durante el ciclo, mientras esta referencia adicional está activa. Esto desencadena un clon, y eso explica lo que está sucediendo aquí.

Aquí hay un excelente artículo para otro efecto secundario de este comportamiento de copia en escritura: El Operador PHP Ternario: ¿Rápido o no?

linepogl
fuente
parece correcto, hice un ejemplo que demuestra que: codepad.org/OCjtvu8r es una diferencia de su ejemplo: no se copia si cambia el valor, solo si cambia las claves.
zb '
De hecho, esto explica todo el comportamiento que se muestra arriba, y puede ilustrarse muy bien llamando each()al final del primer caso de prueba, donde vemos que el puntero de la matriz de la matriz original apunta al segundo elemento, ya que la matriz se modificó durante La primera iteración. Esto también parece demostrar que foreachmueve el puntero de la matriz antes de ejecutar el bloque de código del bucle, lo que no esperaba; hubiera pensado que haría esto al final. Muchas gracias, esto me aclara muy bien.
DaveRandom
49

Algunos puntos a tener en cuenta al trabajar con foreach():

a) foreachfunciona en la copia prospectiva de la matriz original. Significa que foreach()tendrá almacenamiento de datos COMPARTIDO hasta o a menos prospected copyque no se haya creado para cada Notas / Comentarios del usuario .

b) ¿Qué desencadena una copia prospectada ? Se crea una copia prospectada en función de la política de copy-on-write, es decir, cada vez que foreach()se cambia una matriz que se pasa , se crea un clon de la matriz original.

c) La matriz original y el foreach()iterador tendrán DISTINCT SENTINEL VARIABLES, es decir, uno para la matriz original y otro para foreach; ver el código de prueba a continuación. SPL , Iteradores e Iterador de matriz .

Pregunta de desbordamiento de pila ¿ Cómo asegurarse de que el valor se restablezca en un bucle 'foreach' en PHP? aborda los casos (3,4,5) de su pregunta.

El siguiente ejemplo muestra que cada () y reset () NO afectan las SENTINELvariables (for example, the current index variable)del foreach()iterador.

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

Salida:

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
fuente
2
Tu respuesta no es del todo correcta. foreachopera en una copia potencial de la matriz, pero no realiza la copia real a menos que sea necesaria.
linepogl
¿Le gustaría demostrar cómo y cuándo se crea esa copia potencial a través del código? Mi código demuestra que foreachestá copiando la matriz el 100% del tiempo. Estoy ansioso por saber Gracias por sus comentarios
sakhunzai
Copiar una matriz cuesta mucho. Intente contar el tiempo que lleva iterar una matriz con 100000 elementos usando foro foreach. No verá ninguna diferencia significativa entre los dos, porque no se realiza una copia real.
linepogl
Entonces supondría que hay SHARED data storagereservado hasta o a menos copy-on-write, pero (de mi fragmento de código) es evidente que siempre habrá DOS conjuntos de SENTINEL variablesuno para el original arrayotro foreach. Gracias, eso tiene sentido
sakhunzai
1
sí, es una copia "prospectiva", es decir, una copia "potencial". No está protegida como usted sugirió
sakhunzai
33

NOTA PARA PHP 7

Para actualizar esta respuesta, ya que ha ganado algo de popularidad: esta respuesta ya no se aplica a partir de PHP 7. Como se explica en " Cambios incompatibles con versiones anteriores ", en PHP 7 foreach funciona en la copia de la matriz, por lo que cualquier cambio en la matriz en sí no se reflejan en el bucle foreach. Más detalles en el enlace.

Explicación (cita de php.net ):

La primera forma recorre la matriz dada por array_expression. En cada iteración, el valor del elemento actual se asigna a $ value y el puntero interno de la matriz avanza en uno (por lo que en la próxima iteración, verá el siguiente elemento).

Entonces, en su primer ejemplo, solo tiene un elemento en la matriz, y cuando se mueve el puntero, el siguiente elemento no existe, por lo tanto, después de agregar un nuevo elemento, cada extremo termina porque ya "decidió" que es el último elemento.

En su segundo ejemplo, comienza con dos elementos, y el bucle foreach no está en el último elemento, por lo que evalúa la matriz en la próxima iteración y, por lo tanto, se da cuenta de que hay un nuevo elemento en la matriz.

Creo que todo esto es consecuencia de En cada iteración, parte de la explicación en la documentación, lo que probablemente significa que foreachhace toda la lógica antes de llamar al código {}.

Caso de prueba

Si ejecuta esto:

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

Obtendrá esta salida:

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

Lo que significa que aceptó la modificación y la realizó porque se modificó "a tiempo". Pero si haces esto:

<?
    $array = Array(
        'foo' => 1,
        'bar' => 2
    );
    foreach($array as $k=>&$v) {
        if ($k=='bar') {
            $array['baz']=3;
        }
        echo $v." ";
    }
    print_r($array);
?>

Conseguirás:

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

Lo que significa que la matriz se modificó, pero dado que la modificamos cuando foreachya estaba en el último elemento de la matriz, "decidió" no hacer más bucles, y aunque agregamos un nuevo elemento, lo agregamos "demasiado tarde" y no fue atravesado.

La explicación detallada se puede leer en ¿Cómo funciona realmente PHP 'foreach'? lo que explica lo interno detrás de este comportamiento.

dkasipovic
fuente
77
¿Leíste el resto de la respuesta? Tiene mucho sentido que foreach decida si se repetirá otra vez antes de que incluso ejecute el código.
dkasipovic
2
No, la matriz se modifica, pero es "demasiado tarde" ya que foreach ya "piensa" que está en el último elemento (que está al comienzo de la iteración) y que ya no se repetirá. Donde en el segundo ejemplo, no está en el último elemento al comienzo de la iteración y se evalúa nuevamente al comienzo de la próxima iteración. Estoy tratando de preparar un caso de prueba.
dkasipovic
1
@AlmaDo Mire lxr.php.net/xref/PHP_TRUNK/Zend/zend_vm_def.h#4509 Siempre se establece en el siguiente puntero cuando itera. Entonces, cuando llegue a la última iteración, se marcará como terminado (a través del puntero NULL). Cuando agrega una clave en la última iteración, foreach no lo notará.
bwoebi
1
@DKasipovic no. No hay una explicación completa y clara allí (al menos por ahora, puede que me equivoque)
Alma Do
44
En realidad, parece que @AlmaDo tiene una falla en la comprensión de su propia lógica ... Su respuesta está bien.
bwoebi
15

Según la documentación proporcionada por el manual de PHP.

En cada iteración, el valor del elemento actual se asigna a $ v y el
puntero interno de la matriz avanza en uno (por lo tanto, en la próxima iteración, verá el siguiente elemento).

Entonces, según su primer ejemplo:

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

$arraytener solo un elemento, por lo que según la ejecución foreach, 1 asigno $vy no tiene ningún otro elemento para mover el puntero

Pero en tu segundo ejemplo:

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

$arraytiene dos elementos, por lo que ahora $ array evalúa los índices cero y mueve el puntero por uno. Para la primera iteración del bucle, se agrega $array['baz']=3;como pase por referencia.

usuario3535130
fuente
13

Gran pregunta, porque muchos desarrolladores, incluso los experimentados, están confundidos por la forma en que PHP maneja las matrices en bucles foreach. En el bucle foreach estándar, PHP hace una copia de la matriz que se usa en el bucle. La copia se descarta inmediatamente después de que finaliza el bucle. Esto es transparente en la operación de un bucle foreach simple. Por ejemplo:

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

Esto produce:

apple
banana
coconut

Por lo tanto, se crea la copia pero el desarrollador no se da cuenta, porque la matriz original no está referenciada dentro del ciclo o después de que finaliza el ciclo. Sin embargo, cuando intenta modificar los elementos en un bucle, descubre que no se modifican cuando termina:

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

print_r($set);

Esto produce:

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

Los cambios del original no pueden ser avisos, en realidad no hay cambios del original, a pesar de que claramente asignó un valor a $ item. Esto se debe a que está operando en $ item tal como aparece en la copia de $ set en la que se está trabajando. Puede anular esto tomando $ item por referencia, así:

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

Esto produce:

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

Por lo tanto, es evidente y observable que, cuando $ item se opera por referencia, los cambios realizados en $ item se realizan a los miembros del conjunto $ original. El uso de $ item por referencia también evita que PHP cree la copia de la matriz. Para probar esto, primero mostraremos un script rápido que demuestra la copia:

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

Esto produce:

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

Como se muestra en el ejemplo, PHP copió $ set y lo usó para recorrer, pero cuando se usó $ set dentro del loop, PHP agregó las variables a la matriz original, no a la matriz copiada. Básicamente, PHP solo está utilizando la matriz copiada para la ejecución del bucle y la asignación de $ item. Debido a esto, el ciclo anterior solo se ejecuta 3 veces, y cada vez que agrega otro valor al final del conjunto $ original, dejando el conjunto $ original con 6 elementos, pero nunca ingresando un ciclo infinito.

Sin embargo, ¿qué pasaría si hubiéramos usado $ item por referencia, como mencioné antes? Un solo carácter agregado a la prueba anterior:

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

Resultados en un bucle infinito. Tenga en cuenta que en realidad es un bucle infinito, tendrá que eliminar el script usted mismo o esperar a que su sistema operativo se quede sin memoria. Agregué la siguiente línea a mi script para que PHP se quedara sin memoria muy rápidamente, le sugiero que haga lo mismo si va a ejecutar estas pruebas de bucle infinito:

ini_set("memory_limit","1M");

Entonces, en este ejemplo anterior con el bucle infinito, vemos la razón por la cual PHP se escribió para crear una copia de la matriz para realizar un bucle. Cuando se crea una copia y se usa solo por la estructura de la construcción del bucle, la matriz permanece estática durante la ejecución del bucle, por lo que nunca se encontrará con problemas.

Hrvoje Antunović
fuente
7

El bucle foreach de PHP se puede usar con Indexed arrays, Associative arraysyObject public variables .

En foreach loop, lo primero que hace php es crear una copia de la matriz que se debe repetir. PHP luego itera sobre esta nueva copymatriz en lugar de la original. Esto se demuestra en el siguiente ejemplo:

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

Además de esto, php también permite su uso iterated values as a reference to the original array value. Esto se demuestra a continuación:

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

Nota: No permite original array indexesser utilizado como references.

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

Pranav Rana
fuente
1
Object public variablesestá mal o, en el mejor de los casos, es engañoso. No puede usar un objeto en una matriz sin la interfaz correcta (por ejemplo, Traversible) y cuando lo hace foreach((array)$obj ..., de hecho está trabajando con una matriz simple, ya no es un objeto.
Christian