Pregunta ¿Cómo funciona PHP 'foreach' realmente?


Déjame prefijar esto diciendo que sé lo que foreach es, 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 "esta es la forma en que bucle una matriz con foreach".


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

Déjame mostrar 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 ciclo continuará para siempre, ya que constantemente estamos presionando elementos en 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, el puntero de la matriz interna se restablece automáticamente al primer elemento de la matriz.

Correcto ... esto parece sugerir que foreach se basa en el puntero de matriz de la matriz de origen. Pero acabamos de demostrar que estamos no funciona con la matriz fuente, ¿derecho? 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)
*/

Por lo tanto, a pesar del hecho de que no estamos trabajando directamente con la matriz de origen, 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 muestra esto. Excepto que esto no puede ser cierto, si lo fue, entonces caso de prueba 1 sería un ciclo para siempre.

El manual de PHP también declara:

Como foreach depende del puntero del arreglo interno, cambiarlo dentro del ciclo puede conducir a un comportamiento inesperado.

Bueno, descubramos qué es ese "comportamiento inesperado" (técnicamente, cualquier comportamiento es inesperado porque 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 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 foreach trabaja con un dupdo de la matriz, pero establece el puntero de matriz de la matriz fuente al final de la matriz después del bucle.

  • ¿Es esto correcto y toda la historia?
  • Si no, ¿qué está haciendo realmente?
  • ¿Hay alguna situación en la que se usen funciones que ajusten el puntero del array (each(), reset() et al.) durante un foreach podría afectar el resultado del ciclo?

1637
2018-04-07 19:33


origen


Respuestas:


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

A continuación, intentaré explicar con precisión cómo funciona la iteración en los diferentes casos. Por mucho, el caso más simple es Traversable objetos, en cuanto a estos foreach es esencialmente solo 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 el Iterator interfaz 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 pedidos y que se recorrerán de acuerdo con este orden (que coincide con el orden de inserción siempre que no utilice algo así como sort) Esto se opone a iterar por el orden natural de las claves (cómo funcionan a menudo las listas en otros idiomas) o al no tener un orden definido (cómo funcionan a menudo los diccionarios en otros idiomas).

Lo mismo también se aplica a los objetos, ya que las propiedades del objeto se pueden ver como otros nombres de propiedades (ordenados) de asignación de diccionarios a sus valores, más cierto control de 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 objetos simples se vuelve muy similar a la iteración de matrices (razón por la cual no estoy discutiendo demasiado 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 $arr se 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 fue una referencia de antemano: $ref =& $arr; foreach ($ref as $v)
  • Los objetos tienen una semántica pasajera by-handle, que para propósitos prácticos debe significar 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 ahora se libera este elemento, se queda con un puntero colgando (lo que generalmente da como resultado una segfault).

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

Como último preliminar, debe tenerse en cuenta que PHP utiliza el recuento de referencias y la función de copiar y escribir para administrar la memoria. Esto significa que si "copia" un valor, 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"). Ver Te están mintiendo para una introducción más extensa sobre este tema.

PHP 5

Puntero de matriz interno y HashPointer

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

Si bien Foreach sí utiliza el IAP, existe una complicación adicional: solo hay un IAP, pero un conjunto puede ser parte de múltiples 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 solo un puntero de matriz interno, foreach realiza los siguientes schenanigans: antes de que se ejecute el cuerpo del bucle, foreach hará una copia de seguridad de un puntero al elemento actual y su hash en un por-foreach HashPointer. Después de que el cuerpo del ciclo se ejecute, el IAP volverá a establecerse en este elemento si aún existe. Sin embargo, si el elemento se ha eliminado, simplemente utilizaremos donde esté actualmente el IAP. Este esquema funciona básicamente como un poco, pero hay un montón de comportamientos extraños que puedes sacar, 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 del current familia de funciones), como tales los cambios en el IAP cuentan como modificaciones en la semántica de copia en escritura. Desafortunadamente, esto significa que foreach se ve forzado en muchos casos a duplicar la matriz sobre la que se está iterando. Las condiciones precisas son:

  1. La matriz no es una referencia (is_ref = 0). Si es una referencia, los cambios son supuesto para propagar, por lo que no debe duplicarse.
  2. La matriz tiene refcount> 1. Si refcount es 1, entonces la matriz no se comparte y somos libres de modificarla directamente.

Si la matriz no está duplicada (is_ref = 0, refcount = 1), solo se incrementará su refcount (*). Además, si se utiliza 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($arr);

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

(*) Incrementar el refcount aquí suena inocuo, pero viola la semántica de copy-on-write (COW): Esto significa que vamos a modificar el IAP de una matriz refcount = 2, mientras que COW dictamina que las modificaciones solo pueden realizarse en refcount = 1 valores. Esta violación da como resultado un cambio de comportamiento visible para el usuario (mientras que COW es normalmente transparente), porque el cambio de IAP en la matriz iterada será observable, pero solo hasta la primera modificación que no sea de IAP en la matriz. En cambio, 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 ciclo, o c) no utilizar el IAP en absoluto ( la solución PHP 7).

Colocar orden de avance

Hay un último detalle de implementación que debe conocer para comprender correctamente los ejemplos de código a continuación. La forma "normal" de iterar a través de una 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 forma ligeramente diferente:

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

A saber, el puntero del array ya se movió hacia adelante antes de el cuerpo del bucle corre. 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 los ejemplos de código que muestran modificaciones durante la iteración siempre desarmarán el siguiente elemento, en lugar de la actual.

Ejemplos: sus casos de prueba

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

El comportamiento de sus casos de prueba es simple 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 recuento. Cuando el cuerpo del bucle posteriormente modifica la matriz (que tiene reccount = 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 lo que foreach modificará el IAP de la $array variable. Al final de la iteración, IAP es NULL (lo que significa iteración completada), que each indica regresando false.

  • En los casos de prueba 4 y 5 ambos each y reset son funciones de referencia por referencia. los $array tiene un refcount=2 cuando se les pasa, entonces tiene que ser duplicado. Como tal foreach estará trabajando en una matriz separada de nuevo.

Ejemplos: efectos de current en foreach

Una buena forma de mostrar los distintos comportamientos de duplicación es observar el comportamiento de current() función dentro de un bucle foreach. Considera este ejemplo:

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

Aquí deberías saber que current() es una función by-ref (en realidad: prefer-ref), aunque no modifique la matriz. Tiene que ser para jugar bien con todas las otras funciones como next que son todos por ref. El paso por referencia implica que la matriz debe separarse y, por lo tanto, $array y la matriz foreach será diferente. La razón por la que obtienes 2 en lugar de 1 también se menciona arriba: foreach avanza el puntero de matriz antes de ejecutando 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 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 (como en el ejemplo anterior). Pero ahora que es una referencia, la matriz ya no tiene que ser duplicada cuando pasa al by-ref current() función. Así current() y foreach trabajan en la misma matriz. Sin embargo, aún ves el comportamiento off-by-one, debido a la manera foreachavanza el puntero.

Obtienes el mismo comportamiento cuando haces iteración por ref .:

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

Aquí la parte importante es que foreach hará $array an 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 refcount de la $array es 2 cuando se inicia el ciclo, por lo que por una vez realmente tenemos que hacer la duplicación por adelantado. Así $array y la matriz utilizada por foreach estará completamente separada desde el principio. Es por eso que obtienes la posición del IAP donde sea 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 el origen de todos nuestros problemas foreach, por lo que sirve para considerar algunos ejemplos para este caso.

Considere estos bucles anidados en la misma matriz (donde la iteración de ref se usa para asegurarse de que realmente es 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 el resultado, porque el elemento 1 fue removido. Lo que probablemente sea inesperado es que el ciclo externo se detenga 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 respaldan en una HashPointer. Después del cuerpo del bucle, se restaurará, pero solo si el elemento aún existe, de lo contrario se utilizará 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 se ha marcado como terminado por el bucle interno.

Otra consecuencia de la HashPointer El mecanismo de copia de seguridad + restauración es aquel que cambia al IAP reset() etc. por lo general no afectan Foreach. Por ejemplo, el siguiente código se ejecuta como si el reset() no estaban presentes 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 reset() para hacer 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 todavía están sanos. La verdadera diversión comienza si recuerdas que HashPointer restaurar usa un puntero al elemento y su hash para determinar si aún existe. Pero: ¡los hash tienen colisiones, y los punteros pueden reutilizarse! Esto significa que, con una cuidadosa elección de las teclas de matriz, podemos hacer foreach cree 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 de acuerdo con las reglas anteriores. Cómo lo que sucede es que 'FYFY' tiene el mismo hash que el elemento eliminado 'EzFY', y el asignador pasa a reutilizar la misma ubicación de memoria para almacenar el elemento. Entonces foreach termina saltando directamente al elemento recién insertado, cortando así el ciclo.

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 la mitad. 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 comenzará a repetir la otra entidad desde el principio una vez que la sustitución haya ocurrido.

PHP 7

Iterables hashtable

Si aún recuerda, el principal problema con la iteración de matriz fue cómo manejar la eliminación de elementos a mitad de iteración. 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 ciclos foreach simultáneos y 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 hashtable seguros y externos. Estos iteradores deben registrarse en la matriz, desde ese punto tienen la misma semántica que IAP: si se elimina un elemento de matriz, todos los iteradores de tabla doble que apuntan a ese elemento se avanzarán al siguiente elemento.

Esto significa que foreach ya no usará el IAP en absoluto. El ciclo foreach no tendrá ningún efecto en los resultados de current() etc. y su propio comportamiento nunca será 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 arreglos. Ahora que el IAP ya no se usa, la iteración de matrices de valor por defecto solo realizará un incremento de recuento (en lugar de duplicar el conjunto) en todos los casos. Si la matriz se modifica durante el ciclo foreach, en ese punto se producirá una duplicación (según copy-on-write) y foreach seguirá 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 se produce un comportamiento diferente, es decir, el caso en el 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 con valores por debajo 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 se reflejarán en el ciclo. En PHP 7 este caso especial se ha ido: una iteración por valor de una matriz siempre sigue trabajando en los elementos originales, sin tener en cuenta las modificaciones durante el ciclo.

Esto, por supuesto, no se aplica a la iteración de referencia por referencia. Si itera por referencia, todas las modificaciones se reflejarán en el ciclo. Curiosamente, lo mismo es cierto para la iteración de valor por defecto 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 manipulación de los objetos (es decir, se comportan como referencias, incluso en contextos con valores por defecto).

Ejemplos

Consideremos algunos ejemplos, comenzando con los casos de prueba:

  • Los casos de prueba 1 y 2 conservan el mismo resultado: la iteración de matriz de valor por defecto siempre sigue trabajando en los elementos originales. (En este caso, incluso el comportamiento de recuento 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 ciclo Tendrá la misma salida antes y después.

  • Los casos de prueba 4 y 5 siguen siendo los mismos: each() y reset() duplicará 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 se compartió).

El segundo conjunto de ejemplos estaba relacionado con el comportamiento de current() bajo diferentes configuraciones de referencia / reconfiguración. Esto ya no tiene sentido, ya que current() no se ve afectado por el bucle, por lo que su valor de retorno siempre permanece igual.

Sin embargo, obtenemos algunos cambios interesantes al considerar modificaciones durante la iteración. Espero que encuentres el nuevo comportamiento más sensato. 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 ciclo externo ya no aborta después de la primera iteración. La razón es que ambos bucles ahora tienen iteradores de tabla doble totalmente separados, y ya no existe ninguna contaminación cruzada de ambos bucles a través de un IAP compartido.

Otro caso de borde raro que se soluciona ahora es el efecto extraño 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 HashPointer saltaba directamente al nuevo elemento, porque "parecía" que era igual que el elemento de eliminación (debido al hash y al puntero colisionantes). Como ya no confiamos en el elemento hash para nada, ya no es un problema.


1378
2018-02-13 13:21



En el ejemplo 3, no modifica la matriz. En todos los demás ejemplos, modifica el contenido o el puntero del arreglo interno. Esto es importante cuando se trata de PHP matrices 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. Al asignar una variable a otra que contiene una matriz se 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 se modifique cualquiera de las variables (copy-on-write).

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 ciclo y luego, ambos se descartan. Ahora puede ver que, en todos los casos excepto en 3, la matriz se modifica durante el ciclo, mientras que esta referencia adicional está activa. Esto activa un clon, ¡y eso explica lo que está sucediendo aquí!

Aquí hay un artículo excelente para otro efecto secundario de este comportamiento de copiado sobre escritura: El operador ternario de PHP: ¿rápido o no?


97
2018-04-07 20:43



Algunos puntos a tener en cuenta cuando se trabaja con foreach():

un) foreach trabaja en el copia prospectada de la matriz original.     Significa que foreach () tendrá el almacenamiento de datos COMPARTIDO hasta o a menos que prospected copy es     no creado foreach Notas / Comentarios del usuario.

b) Lo que desencadena una copia prospectada?     La copia prospectada se crea según la política de copy-on-write, es decir, cada vez que     una matriz pasada a foreach () se cambia, se crea una copia de la matriz original.

c) El array original y el iterador foreach () tendrán DISTINCT SENTINEL VARIABLES, es decir, uno para la matriz original y otro para foreach; vea el código de prueba a continuación. SPL , Iteradoresy Array Iterator.

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

El siguiente ejemplo muestra que cada () y reinicio () NO Afecta SENTINEL variables (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

34
2018-04-07 21:03



NOTA PARA PHP 7

Para actualizar sobre esta respuesta, ya que ha ganado algo de popularidad: Esta respuesta ya no se aplica a partir de PHP 7. Como se explica en el "Cambios hacia atrás incompatibles", en PHP 7 foreach funciona en la copia de la matriz, por lo que los cambios 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 $ valor y   el puntero de matriz interna se avanza en uno (así en el siguiente   iteración, verás el siguiente elemento).

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 que después de agregar el nuevo elemento, foreach finaliza porque ya lo "decidió" como ú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 siguiente iteración y, por lo tanto, se da cuenta de que hay un elemento nuevo en la matriz.

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

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ás esta salida:

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

Lo que significa que aceptó la modificación y la pasó 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 fue modificada, pero desde que la modificamos cuando foreach Ya estaba en el último elemento de la matriz, "decidió" no volver a bucle, y aunque añadimos un nuevo elemento, lo agregamos "demasiado tarde" y no se pasó por el bucle.

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.


22
2018-04-15 08:46



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 interno
  el puntero de matriz se avanza en uno (por lo tanto, en la siguiente iteración, mirará el siguiente elemento).

Entonces, según su primer ejemplo:

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

$array tener solo un elemento, por lo que según la ejecución de foreach, 1 asignar 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);
}

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


8
2018-04-15 09:32



Gran pregunta, porque muchos desarrolladores, incluso los experimentados, están confundidos por la forma en que PHP maneja las matrices en los bucles foreach. En el ciclo foreach estándar, PHP hace una copia de la matriz que se utiliza en el ciclo. La copia se descarta inmediatamente después de que termine el ciclo. Esto es transparente en el funcionamiento 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 se referencia dentro del ciclo o una vez que finaliza el ciclo. 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
)

Cualquier cambio desde el original no puede ser avisos, en realidad no hay cambios desde el original, aunque claramente asignó un valor a $ artículo. Esto se debe a que está operando en $ artículo tal como aparece en la copia de $ set en la que se está trabajando. Puede anular esto agarrando $ artículo 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
)

Entonces, es evidente y observable, cuando $ item es operado por referencia, los cambios hechos a $ item se hacen a los miembros del $ set original. Usar $ item por referencia también evita que PHP cree la copia de la matriz. Para probar esto, primero mostraremos un script rápido que demuestre 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 repetir, pero cuando se usó $ set dentro del ciclo, PHP agregó las variables a la matriz original, no la matriz copiada. Básicamente, PHP solo usa 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 agrega otro valor al final del $ set original, dejando el $ original con 6 elementos, pero nunca ingresando un bucle infinito.

Sin embargo, ¿qué pasaría si hubiésemos usado $ artículo 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 ciclo 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 quedara sin memoria muy rápidamente, le sugiero que haga lo mismo si va a ejecutar estas pruebas infinitas de bucle:

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

Entonces, en este ejemplo anterior con el ciclo infinito, vemos la razón por la cual PHP fue escrito para crear una copia de la matriz para repetir. Cuando se crea una copia y solo se utiliza 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.


5
2018-04-21 08:44



PHP foreach loop se puede usar con Indexed arrays, Associative arrays y Object public variables.

En foreach loop, lo primero que php hace es crear una copia de la matriz que se va a iterar. PHP luego itera sobre este nuevo copy de la matriz 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 permite usar iterated values as a reference to the original array value también. 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 indexes para ser utilizado como references.

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


5
2017-11-13 14:08