loops array - ¿Cómo funciona PHP 'foreach' realmente funciona?




w3schools mysql (7)

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

A continuación, trataré de explicar con precisión cómo funciona la iteración en los diferentes casos. Con mucho, el caso más simple son los objetos Traversable , ya que para estos foreach es esencialmente solo la sintaxis de azúcar para el código en 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 interfaz del Iterator en el nivel C.

La iteración de matrices y objetos simples es significativamente más complicada. En primer lugar, se debe tener en cuenta que en PHP las "matrices" son realmente diccionarios ordenados y que se recorrerán de acuerdo con este orden (que coincide con el orden de inserción siempre y cuando no se use algo similar). Esto se opone a la iteración por el orden natural de las teclas (cómo suelen funcionar las listas en otros idiomas) o al no tener ningún orden definido (cómo funcionan los diccionarios en otros idiomas).

Lo mismo se aplica a los objetos, ya que las propiedades del objeto se pueden ver como otro diccionario (ordenado) que asigna nombres de propiedades a sus valores, además de un manejo de la visibilidad. En la mayoría de los casos, las propiedades del objeto no se almacenan realmente 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 los objetos simples se vuelve muy similar a la iteración de los arreglos (por lo que no estoy discutiendo mucho la iteración de los objetos simples aquí).

Hasta ahora tan bueno. 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 realiza una iteración por referencia utilizando foreach ($arr as &$v) entonces $arr se convierte en una referencia y puede cambiarla durante la iteración.
  • En PHP 5, lo mismo se aplica incluso si se itera por valor, pero la matriz fue una referencia de antemano: $ref =& $arr; foreach ($ref as $v) $ref =& $arr; foreach ($ref as $v)
  • Los objetos tienen una semántica de pases de manejo, que para fines prácticos significa que se comportan como referencias. Así que los objetos siempre pueden ser cambiados 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 utiliza un puntero para realizar un seguimiento del elemento de matriz en el que se encuentra actualmente. Si este elemento ahora está liberado, te queda un puntero colgante (que generalmente resulta en una falla de seguridad).

Hay diferentes maneras 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 complejo de PHP 7 resulta en un comportamiento más predecible y consistente.

Como último preliminar, se debe tener en cuenta que PHP utiliza el conteo de referencias y la copia en escritura para administrar la memoria. Esto significa que si "copia" un valor, en realidad simplemente reutiliza el valor anterior e incrementa su recuento de referencia (refcount). Solo una vez que realice algún tipo de modificación, se realizará una copia real (llamada "duplicación"). Vea Se le está 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 de manera adecuada: cada vez que se elimina un elemento, se verificará si el IAP apunta a este elemento. Si lo hace, se avanza al siguiente elemento en su lugar.

Si bien foreach hace uso del IAP, existe una complicación adicional: solo hay un IAP, pero una matriz puede ser parte de varios bucles 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) {
        // ...
    }
}

Para admitir dos bucles simultáneos con un solo puntero de matriz interno, foreach realiza los siguientes esquemas: Antes de que se ejecute el cuerpo del bucle, foreach creará un puntero al elemento actual y su hash en un HashPointer por foreach. Después de que se ejecute el cuerpo del bucle, el IAP volverá a establecerse en este elemento si aún existe. Sin embargo, si el elemento ha sido eliminado, solo usaremos el lugar donde se encuentre actualmente el IAP. Este esquema funciona en su mayor parte, pero es un poco extraño, pero puedes demostrarlo a continuación.

Duplicación de matrices

El IAP es una característica visible de una matriz (expuesta a través de la familia current de funciones), ya que dichos cambios en el IAP cuentan como modificaciones en la semántica de copia en escritura. Desafortunadamente, esto significa que foreach en muchos casos está 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 es una referencia, entonces se supone que los cambios en ella se propagan, por lo que no se deben duplicar.
  2. La matriz tiene refcount> 1. Si refcount es 1, la matriz no se comparte y podemos modificarla directamente.

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

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

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

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

Aquí, $arr se duplicará para evitar que los cambios de IAP en $arr $outerArr a $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 es un artefacto de la implementación subóptima (no existe ninguna preocupación sobre la modificación durante la iteración aquí, por lo que no necesitamos utilizar el IAP en primer lugar).

(*) Incrementar el refcount aquí parece 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 el COW normalmente es 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 su lugar, las tres opciones "válidas" habrían sido a) duplicar siempre, b) no incrementar el refcount y, por lo tanto, permitir que la matriz iterada se modifique arbitrariamente en el bucle, o c) no use el IAP en absoluto ( la solución PHP 7).

Posicionar avance orden

Hay un último detalle de la implementación que debe tener en cuenta para comprender correctamente los ejemplos de código a continuación. La forma "normal" de realizar un bucle a través de 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 , siendo 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();
}

Es decir, el puntero de matriz ya se ha movido hacia delante 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 que los ejemplos de código que muestran modificaciones durante la iteración siempre desarmarán el siguiente elemento, en lugar del actual.

Ejemplos: sus casos de prueba

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

El comportamiento de sus casos de prueba es fácil de explicar en este punto:

  • En los casos de prueba, 1 y 2 $array comienza con refcount = 1, por lo que no será duplicado por foreach: solo se incrementa el refcount. Cuando el cuerpo del bucle modifica posteriormente la matriz (que tiene refcount = 2 en ese punto), la duplicación se producirá 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 se duplica, por lo que foreach modificará el IAP de la variable $array . Al final de la iteración, el IAP es NULL (es decir, iteración realizada), que each indica devolviendo false .

  • En los casos de prueba 4 y 5, each y el reset son funciones de referencia. El $array tiene un refcount=2 cuando se pasa a ellos, por lo que tiene que ser duplicado. Como tal, cada uno volverá a trabajar en una matriz separada.

Ejemplos: Efectos de la current en foreach.

Una buena manera de mostrar los diversos comportamientos de duplicación es observar el comportamiento de la función current() dentro de un bucle foreach. Considera 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 de referencia (en realidad: prefer-ref), aunque no modifique la matriz. Tiene que ser para jugar bien con todas las otras funciones como las next que son todas por referencia. El paso por referencia implica que la matriz debe estar separada y, por lo tanto, $array y foreach-array serán diferentes. La razón por la que obtiene 2 lugar de 1 también se menciona anteriormente: foreach avanza 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, foreach ya adelantó el puntero al segundo.

Ahora probemos 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 (como en la anterior). Pero ahora que es una referencia, la matriz ya no tiene que duplicarse cuando se pasa a la función by-ref current() . Por lo tanto, current() y foreach funcionan en la misma matriz. Sin embargo, aún se ve el comportamiento off-by-one, debido a la forma en que cada uno avanza el puntero.

Obtienes el mismo comportamiento al hacer la iteración by-ref:

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

Aquí lo importante es que foreach hará $array an is_ref = 1 cuando se itere por referencia, así 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 refcount de $array es 2 cuando se inicia el ciclo, así que por una vez tenemos que hacer la duplicación por adelantado. Por lo tanto, $array y la matriz utilizada por foreach estarán completamente separadas 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.

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

Considere estos bucles anidados sobre la misma matriz (donde se usa la iteración by-ref 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 se eliminó el elemento 1 . 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 actual de IAP y el hash se realiza en un HashPointer . Después del cuerpo del bucle, se restaurará, pero solo si el elemento aún existe, de lo contrario, se usará 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 HashPointer copia de seguridad y restauración de HashPointer es que los cambios en el IAP aunque reset() etc., por lo general no afectan a cada persona. Por ejemplo, el siguiente código se ejecuta como si reset() no estuviera presente en absoluto:

$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, mientras reset() modifica temporalmente el IAP, se restaurará al elemento foreach actual después del cuerpo del bucle. Para forzar que reset() haga un efecto en el bucle, debe eliminar adicionalmente el elemento actual, para 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 siguen cuerdos. La verdadera diversión comienza si recuerdas que la restauración HashPointer usa un puntero al elemento y su hash para determinar si aún existe. Pero: ¡Los hash tienen colisiones y los punteros se pueden reutilizar! Esto significa que, con una elección cuidadosa de las claves de matriz, podemos hacer que cada uno crea que un elemento que se ha eliminado todavía existe, 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 1, 1, 3, 4 acuerdo 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. Así que foreach termina saltando directamente al elemento recién insertado, cortando así el bucle.

Sustituyendo la entidad iterada durante el bucle

Un último caso extraño que me gustaría mencionar es que PHP le permite sustituir la entidad iterada durante el ciclo. Por lo tanto, puede comenzar a iterar en una matriz y luego reemplazarla con otra matriz a la mitad. O comience a iterar en una matriz y luego reemplácelo 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 comenzará a iterar la otra entidad desde el principio una vez que se haya producido la sustitución.

PHP 7

Iteradores hash

Si aún recuerda, el principal problema con la iteración de matrices fue cómo manejar la eliminación de elementos de la iteración media. PHP 5 usó un único puntero de matriz interno (IAP) para este propósito, que era algo subóptimo, ya que un puntero de matriz tenía que estirarse para admitir 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 los cuales tienen la misma semántica que la 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 foreach ya no usará el IAP en absoluto . El bucle foreach no tendrá ningún efecto en los resultados de current() etc. y su propio comportamiento nunca se verá afectado por funciones como reset() etc.

Duplicación de matrices

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

En la mayoría de los casos, este cambio es transparente y no tiene más efecto que un mejor rendimiento. Sin embargo, hay una ocasión en la que resulta en un comportamiento diferente, a saber, el caso en que la matriz fue una referencia de antemano:

$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 ninguna duplicación, por lo que todas las modificaciones de la matriz durante la iteración serían reflejadas por el bucle. En PHP 7, este caso especial ha desaparecido: una iteración por valor de una matriz siempre seguirá trabajando en los elementos originales, sin tener en cuenta ninguna modificación 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 sin formato:

$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 mediante el manejo (es decir, 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 la matriz por valor siempre sigue trabajando en los elementos originales. (En este caso, incluso el comportamiento de refcounting y duplicación es exactamente el mismo entre PHP 5 y PHP 7).

  • Cambios en el caso de prueba 3: Foreach ya no usa el IAP, por lo que each() 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 foreach todavía usa la matriz original. (No es que el cambio de IAP hubiera importado, incluso si la matriz fuera compartida).

El segundo conjunto de ejemplos se relacionó con el comportamiento de current() en diferentes configuraciones de referencia / refcounting. Esto ya no tiene sentido, ya que current() se ve afectado por el bucle, por lo que su valor de retorno siempre se mantiene igual.

Sin embargo, obtenemos algunos cambios interesantes al considerar 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 aborta 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 ha solucionado ahora, es el efecto impar que obtienes cuando eliminas y agregas 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 el mismo que el elemento eliminar (debido al hash y el puntero que colisionaban). Como ya no dependemos del elemento hash para nada, esto ya no es un problema.

Permítame prefijar esto diciendo que sé lo que es foreach , lo que 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 hace un bucle de una matriz con foreach ".

Durante mucho tiempo asumí que foreach trabajaba con la propia matriz. Luego encontré muchas referencias al hecho de que funciona con una copia de la matriz, y desde entonces asumí que este es el final de la historia. Pero recientemente tuve una discusión sobre el tema, y ​​después de una pequeña experimentación descubrí que esto no era en realidad un 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 de origen; de lo contrario, el bucle continuaría para siempre, ya que estamos presionando constantemente los elementos en la matriz durante el bucle. 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 de origen durante el bucle, de lo contrario veríamos los valores modificados durante el bucle. Pero...

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

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

Correcto ... esto parece sugerir que foreach basa en el puntero de matriz de la matriz de origen. Pero acabamos de demostrar que no estamos trabajando con la matriz de origen , ¿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 de que no estamos trabajando directamente con la matriz de origen, estamos trabajando directamente con el puntero de la matriz de origen; el hecho de que el puntero esté al final de la matriz al final del bucle lo muestra. Excepto que esto no puede ser cierto: si lo fuera, entonces el caso de prueba 1 se interrumpiría para siempre.

El manual de PHP también establece:

Como foreach se basa en el puntero de la matriz interna, cambiarlo dentro del bucle puede provocar un comportamiento inesperado.

Bueno, averigüemos 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 correcta simplemente mirando el código fuente de PHP, agradecería que alguien lo tradujera al inglés para mí.

Me parece que foreach trabaja con una copia de la matriz, pero establece el puntero de matriz 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 ajusten el puntero de la matriz ( each() , reset() y otros) durante un foreach podría afectar el resultado del bucle?

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

En cada iteración, el valor del elemento actual se asigna a $ vy el
puntero de la matriz interna avanza en uno (de modo que en la siguiente iteración, verá el siguiente elemento).

Así como por su primer ejemplo:

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

$arraytiene solo un elemento, por lo que según la ejecución de foreach, asigno a $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 en uno. Para la primera iteración del bucle, se agrega $array['baz']=3;como pase por referencia.


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

a) foreach trabaja en la copia prospectiva de la matriz original. Significa que foreach () tendrá un almacenamiento de datos COMPARTIDO hasta que, a menos que no se cree una prospected copy manual .

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

c) La matriz original y el iterador foreach () tendrán DISTINCT SENTINEL VARIABLES , es decir, una para la matriz original y otra para foreach; Consulte el código de prueba a continuación. SPL , Iterators e Iterador de Array .

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 tu pregunta.

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

$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

En el ejemplo 3 no modificas la matriz. En todos los demás ejemplos, modifica el contenido o el puntero de matriz interno. 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. La asignación de una variable a otra que contiene 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 solo tendrá lugar cuando se modifique cualquiera de las variables (copia 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 foreach crea algún tipo de iterador con una referencia a la matriz. Esta referencia funciona exactamente como la variable $b en mi ejemplo. Sin embargo, el iterador junto con la referencia en vivo solo durante el bucle y luego, ambos se descartan. Ahora puede ver que, en todos los casos, excepto 3, la matriz se modifica durante el bucle, mientras esta referencia adicional está activa. ¡Esto activa un clon, y eso explica lo que está pasando aquí!

Este es un excelente artículo sobre otro efecto secundario de este comportamiento de copia en escritura: El operador PHP ternario: ¿Rápido o no?


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

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

Esto produce:

apple
banana
coconut

Así que la copia se crea pero el desarrollador no se da cuenta, porque no se hace referencia a la matriz original dentro del bucle o después de que el bucle termina Sin embargo, cuando intenta modificar los elementos en un bucle, encuentra que no están modificados 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 del conjunto de $ en el 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, cuando $ item se opera por referencia, los cambios realizados en $ item se realizan a los miembros del conjunto original de $. 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 muestra 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 hacer un ciclo, pero cuando se usó $ set dentro del bucle, 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 bucle anterior solo se ejecuta 3 veces, y cada vez que agrega otro valor al final del conjunto original de $, deja al conjunto original con 6 elementos, pero nunca ingresa a un bucle infinito.

Sin embargo, ¿qué pasaría si hubiéramos usado $ artículo por referencia, como mencioné anteriormente? Un solo carácter añadido 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 esto realmente es un bucle infinito, tendrá que matar 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 quede sin memoria muy rápidamente, 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 que se escribió PHP para crear una copia de la matriz para que se desplace. Cuando una copia es creada y utilizada solo por la estructura de la propia construcción de bucle, la matriz permanece estática durante la ejecución del bucle, por lo que nunca se encontrará con problemas.


NOTA PARA PHP 7

Para actualizar esta respuesta, ya que ha ganado cierta 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 ):

El primer formulario recorre la matriz dada por array_expression. En cada iteración, el valor del elemento actual se asigna a $ valor y el puntero de la matriz interna avanza en uno (de modo que en la siguiente iteración, verá el elemento siguiente).

Por lo tanto, 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, foreach termina porque ya "decidió" que era el último elemento.

En su segundo ejemplo, comienza con dos elementos, y foreach loop no está en el último elemento, por lo que evalúa la matriz en la siguiente 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 revisó 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 como la modificamos cuando foreachya estaba en el último elemento de la matriz, "decidió" no hacer un bucle más, y aunque agregamos un elemento nuevo, lo agregamos "demasiado tarde" y no fue en bucle a través de

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


El compilador declara la variable de una manera que lo hace altamente propenso a un error que a menudo es difícil de encontrar y depurar, mientras que no produce beneficios perceptibles.

Su crítica está totalmente justificada.

Discuto este problema en detalle aquí:

Cierre sobre la variable de bucle considerada dañina.

¿Hay algo que pueda hacer con los bucles foreach de esta manera que no podría hacer si se compilaran con una variable de ámbito interno? ¿o es solo una elección arbitraria que se realizó antes de que los métodos anónimos y las expresiones lambda estuvieran disponibles o fueran comunes, y que no se hayan revisado desde entonces?

El último. La especificación C # 1.0 en realidad no dijo si la variable del bucle estaba dentro o fuera del cuerpo del bucle, ya que no hizo una diferencia observable. Cuando se introdujo la semántica de cierre en C # 2.0, se tomó la decisión de colocar la variable de bucle fuera del bucle, de manera consistente con el bucle "for".

Creo que es justo decir que todos lamentan esa decisión. Este es uno de los peores "errores" en C #, y vamos a tomar el cambio decisivo para solucionarlo. En C # 5, la variable de bucle foreach estará lógicamente dentro del cuerpo del bucle y, por lo tanto, los cierres obtendrán una copia nueva cada vez.

El bucle for no se modificará, y el cambio no se "adaptará" a las versiones anteriores de C #. Por lo tanto, debe seguir teniendo cuidado al utilizar este idioma.





php loops foreach iteration php-internals