¿Cómo funciona realmente PHP 'foreach'?

Déjame anteponer esto diciendo que sé qué foreach es, hace y cómo usarlo. Esta pregunta se refiere a cómo funciona debajo del capó, y no quiero ninguna respuesta como "así es como se hace un bucle en una matriz con foreach".


Durante mucho tiempo asumí que foreach trabajado con la propia matriz. Luego encontré muchas referencias al hecho de que funciona con un copia 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 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ía para siempre, ya que estamos constantemente 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, el puntero de matriz interno 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 somos no funciona con la matriz de origen, ¿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)
*/

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 muestra. Excepto que esto no puede ser cierto, si lo fuera, entonces caso de prueba 1 bucle para siempre.

El manual de PHP también establece:

Como foreach se basa en el puntero de matriz interno, 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 que pueda extraer una conclusión adecuada simplemente mirando el código fuente de PHP, agradecería que alguien me lo tradujera al inglés.

Me parece que foreach trabaja con un 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 es así, ¿qué está haciendo realmente?
  • ¿Existe alguna situación en la que se utilicen funciones que ajustan el puntero de matriz (each(), reset() et al.) durante un foreach podría afectar el resultado del ciclo?

preguntado el 07 de abril de 12 a las 20:04

@DaveRandom Hay un php-internos probablemente debería ir con esta etiqueta, pero dejaré que usted decida cuál de las otras 5 etiquetas reemplazará. -

parece VACA, sin eliminar el identificador -

Al principio pensé "Dios, otra pregunta de novato". Lea los documentos... hm, comportamiento claramente indefinido«. Luego leo la pregunta completa, y debo decir: me gusta. Te has esforzado bastante y has escrito todos los casos de prueba. PD. ¿Son iguales los casos de prueba 4 y 5? -

Solo un pensamiento sobre por qué tiene sentido que se toque el puntero de la matriz: PHP necesita restablecer y mover el puntero de matriz interno de la matriz original junto con la copia, porque el usuario puede solicitar una referencia al valor actual (foreach ($array as &$value)) - PHP necesita conocer la posición actual en la matriz original aunque en realidad esté iterando sobre una copia. -

@Sean: En mi humilde opinión, la documentación de PHP es realmente bastante mala para describir los matices de las características centrales del lenguaje. Pero eso es, quizás, porque muchos casos especiales ad-hoc se incorporan al lenguaje ... -

7 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 diferentes casos. Con 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 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 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 no tener ningún orden definido (cómo funcionan a menudo los diccionarios en otros idiomas).

Lo mismo se aplica también a los objetos, ya que las propiedades del objeto se pueden ver como otro diccionario (ordenado) que asigna nombres de propiedad a sus valores, además de cierto control de visibilidad. En la mayoría de los casos, las propiedades del objeto no se almacenan realmente de esta forma bastante ineficaz. 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 mucho sobre la iteración de objetos simples aquí).

Hasta ahora tan bueno. Iterar en un diccionario no puede ser demasiado difícil, ¿verdad? Los problemas comienzan cuando se da 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) luego $arr se convierte en una referencia y puede cambiarlo durante la iteración.
  • En PHP 5 se aplica lo mismo incluso si itera por valor, pero la matriz era una referencia de antemano: $ref =& $arr; foreach ($ref as $v)
  • Los objetos tienen semántica de paso indirecta, 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 de permitir modificaciones durante la iteración es el caso en el que se elimina el elemento en el que se encuentra actualmente. Digamos que usa un puntero para realizar un seguimiento de en qué elemento de la matriz se encuentra actualmente. Si ahora se libera este elemento, se queda con un puntero colgando (lo que generalmente resulta en una falla de segmento).

Hay diferentes formas de solucionar 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 era bastante tonto y conducía a todo tipo de problemas extraños en casos extremos, mientras que el enfoque más complicado de PHP 7 da como resultado un comportamiento más predecible y consistente.

Como último preliminar, debe tenerse 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"). 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 correctamente: siempre que se elimine un elemento, se verificará si el IAP apunta a este elemento. Si lo hace, se avanza al siguiente elemento en su lugar.

Aunque la foreach hace uso del IAP, hay una complicación adicional: solo hay un IAP, pero una matriz puede ser parte de múltiples foreach bucles:

// 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 las siguientes travesuras: 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 per-foreach HashPointer. Una vez que se ejecuta el cuerpo del bucle, el IAP se volverá a establecer en este elemento si aún existe. Sin embargo, si el elemento se ha eliminado, solo lo usaremos donde se encuentre actualmente el IAP. Este esquema funciona en su mayor parte, pero hay muchos comportamientos extraños que puedes sacar de él, algunos de los cuales demostraré a continuación.

Duplicación de matrices

El IAP es una característica visible de una matriz (expuesta a través del current familia de funciones), ya que tales cambios en el IAP cuentan como modificaciones en la semántica de copia en escritura. Esto, desafortunadamente, significa que foreach en 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 es una referencia, los cambios son Supuesto para propagarse, por lo que no debe duplicarse.
  2. La matriz tiene refcount> 1. Si refcount es 1, entonces la matriz no se comparte y podemos modificarla directamente.

Si la matriz no está duplicada (is_ref = 0, refcount = 1), entonces solo su refcount se incrementará (*). Además, si foreach por referencia, entonces 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 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 preocupación de modificación durante la iteración aquí, por lo que realmente no necesitamos usar el IAP en primer lugar).

(*) Incrementando el refcount aquí suena inofensivo, 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 dicta que las modificaciones solo se pueden realizar en valores refcount = 1. 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 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 así permitir que la matriz iterada se modifique arbitrariamente en el bucle o c) no use el IAP en absoluto (la solución PHP 7).

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

Es decir, el puntero de matriz ya se movió hacia adelante antes se ejecuta 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 unset al siguiente elemento, en lugar del actual.

Ejemplos: sus casos de prueba

Los tres aspectos descritos anteriormente deberían proporcionarle una impresión mayoritariamente completa de las idiosincrasias del foreach implementació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 $array comienza con refcount = 1, por lo que no será duplicado por foreach: Solo el refcount se incrementa. Cuando el cuerpo del ciclo posteriormente modifica la matriz (que tiene refcount = 2 en ese punto), la duplicación ocurrirá en ese punto. Foreach seguirá trabajando en una copia sin modificar de $array.

  • En el caso de prueba 3, una vez más, la matriz no se duplica, por lo tanto foreach modificará el IAP del $array variable. Al final de la iteración, el IAP es NULL (lo que significa que se ha realizado la iteración), que each indica volviendo false.

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

Ejemplos: efectos de current en foreach

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

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

Aquí debes saber que current() es una función by-ref (en realidad: prefer-ref), aunque no modifica la matriz. Tiene que ser así para jugar bien con todas las demás funciones como next que son todos por ref. El paso por referencia implica que la matriz debe separarse y, por lo tanto, $array y el foreach-array 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 ejecutando el código de usuario, no después. Entonces, aunque el código está en el primer elemento, foreach ya 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 (como se muestra arriba). Pero ahora que es una referencia, la matriz ya no tiene que duplicarse al pasar a la referencia current() función. Por lo tanto current() y foreach trabajar en la misma matriz. Sin embargo, todavía ve el comportamiento de apagado por uno, debido a la forma foreach avanza el puntero.

Obtienes el mismo comportamiento cuando haces una iteración por referencia:

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 itera por referencia, por lo que básicamente tienes 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 del $array es 2 cuando se inicia el bucle, así que por una vez tenemos que hacer la duplicación por adelantado. Por lo tanto $array y la matriz utilizada por foreach estará completamente separada desde el principio. Es por eso que obtiene la posición del IAP dondequiera que estuviera antes del bucle (en este caso, estaba en la primera posición).

Ejemplos: modificación durante la iteración

Tratar de tener en cuenta las modificaciones durante la iteración es donde se originaron todos nuestros problemas de 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 por 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 el elemento 1 fue removido. Lo que probablemente sea inesperado es que el bucle exterior se detiene después del primer elemento. ¿Porqué es eso?

La razón detrás de esto es el truco 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 un 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 ciclo externo se ha eliminado, por lo que utilizará el IAP, que ya ha sido marcado como terminado por el ciclo interno.

Otra consecuencia de la HashPointer mecanismo de copia de seguridad + restauración es que los cambios en el IAP a través de reset() etc. generalmente no impactan foreach. Por ejemplo, el siguiente código se ejecuta como si el reset() no estuvieron 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 aún son cuerdos. La verdadera diversión comienza si recuerdas que el HashPointer restore usa un puntero al elemento y su hash para determinar si todavía existe. Pero: los hash tienen colisiones y los punteros se pueden reutilizar. Esto significa que, con una cuidadosa elección de claves de matriz, podemos hacer foreach cree que un elemento que ha sido eliminado aún 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 según las reglas anteriores. como lo que pasa 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, cada uno termina saltando directamente al elemento recién insertado, cortando 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 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 simplemente comenzará a iterar la otra entidad desde el principio una vez que haya ocurrido la sustitución.

PHP 7

Iteradores hash

Si aún recuerda, el principal problema con la iteración de matrices era cómo manejar la eliminación de elementos a mitad de la 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 bucles 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 tabla hash seguros y externos. Estos iteradores deben registrarse en la matriz, desde donde 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 foreach ya no usará el IAP en absoluto. foreach bucle no tendrá absolutamente ningún efecto en los resultados de current() etc. y su propio comportamiento nunca será influenciado 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 el IAP ya no se usa, la iteración de matriz por valor solo hará un refcount incremento (en lugar de duplicar la matriz) en todos los casos. Si la matriz se modifica durante el foreach bucle, en ese punto se producirá una duplicación (de acuerdo con la copia en escritura) 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 da como resultado un comportamiento diferente, es decir, el caso en el que la matriz era 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 matrices de referencia era un caso especial. En este caso, no se produjo ninguna duplicación, por lo que el bucle reflejaría todas las modificaciones del arreglo durante la iteración. En PHP 7, este caso especial se ha ido: una iteración por valor de una matriz siempre siga 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 manejo indirecto de los objetos (es decir, se comportan como referencias incluso en contextos de valor indirecto).

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 por valor siempre sigue funcionando en los elementos originales. (En este caso, incluso refcounting y el comportamiento de 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 siguen siendo los mismos: each() y reset() duplicará la matriz antes de cambiar el IAP, mientras foreach todavía usa la matriz original. (No es que el cambio de IAP hubiera importado, incluso si se compartiera la matriz).

El segundo conjunto de ejemplos estaba relacionado con el comportamiento de current() bajo diferente reference/refcounting configuraciones Esto ya no tiene sentido, ya que current() no se ve afectado en absoluto 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 encuentre 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 ciclo 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 extraño que se solucionó 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 de HashPointer saltó directamente al nuevo elemento porque "parecía" que era el mismo que el elemento eliminado (debido a la colisión de hash y puntero). Como ya no dependemos del hash del elemento para nada, esto ya no es un problema.

Respondido 01 Oct 19, 22:10

@Baba Lo hace. Pasarlo a una función es lo mismo que hacerlo $foo = $array antes del bucle ;) - nikic

Para aquellos de ustedes que no saben lo que es un zval, por favor refiérase a Sara Goleman's blog.golemon.com/2007/01/estás-siendo-mentido-a.html - shu zOMG chen

Corrección menor: lo que llamas Cubo no es lo que normalmente se llama Cubo 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 depósitos, sino en entradas. - unbeli

@unbeli Estoy usando la terminología que usa PHP internamente. El Buckets son parte de una lista doblemente enlazada para colisiones hash y también parte de una lista doblemente enlazada para orden;) - nikic

Gran respuesta. Creo que te refieres a iterate($outerArr); y no iterate($arr); en algún lugar. - lud

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 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. Asignar 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 la clonación se llevará a cabo 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 imaginarse 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 viven 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 esta referencia adicional está activa. ¡Esto desencadena un clon, y eso explica lo que está pasando aquí!

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

Respondido 15 Abr '14, 12:04

parece que tienes razón, hice un ejemplo que demuestra que: codepad.org/OCjtvu8r una diferencia con su ejemplo: no se copia si cambia el valor, solo si cambia las claves. - zb '

De hecho, esto explica todo el comportamiento mostrado anteriormente, y se puede ilustrar muy bien llamando each() al final del primer caso de prueba, donde vemos que el puntero de 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 foreach mueve el puntero de la matriz antes de ejecutar el bloque de código del bucle, lo cual no esperaba; habría pensado que haría esto al final. Muchas gracias, esto me aclara muy bien. - DaveRandom

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

a) foreach trabaja en el copia prospectiva de la matriz original. Significa foreach() tendrá almacenamiento de datos COMPARTIDO hasta o a menos que un prospected copy no se crea notas foreach / comentarios del usuario.

b) ¿Qué desencadena un copia prospectiva? Se crea una copia prospectiva basada en la política de copy-on-write, es decir, siempre que una matriz pasa a foreach() se cambia, se crea un clon de la matriz original.

c) La matriz original y foreach() el iterador tendrá DISTINCT SENTINEL VARIABLES, es decir, uno para la matriz original y otro para foreach; vea el código de prueba a continuación. SPL , Iteradores y 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, XNUMX, XNUMX) de su pregunta.

El siguiente ejemplo muestra que cada () y reset () NO afecta SENTINEL las variables (for example, the current index variable) de la 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

Respondido 23 Abr '19, 22:04

Tu respuesta no es del todo correcta. foreach opera en una copia potencial de la matriz, pero no realiza la copia real a menos que sea necesaria. - lineapogl

¿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 foreach está copiando la matriz el 100% del tiempo. Estoy ansioso por saber. Gracias por tus comentarios - sakhunzai

Copiar una matriz cuesta mucho. Intente contar el tiempo que lleva iterar una matriz con 100000 elementos usando cualquiera for or foreach. No verá ninguna diferencia significativa entre los dos, porque no se realiza una copia real. - lineapogl

Entonces supondría que hay SHARED data storage reservado hasta o a menos que copy-on-write , pero (de mi fragmento de código) es evidente que siempre habrá DOS conjuntos de SENTINEL variables uno para el original array y otro para foreach. Gracias, eso tiene sentido. sakhunzai

sí, eso es una copia "prospectada", es decir, una copia "potencial". No está protegida como sugirió - sakhunzai

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 hacia atrás", en PHP 7, foreach funciona en una copia de la matriz, por lo que cualquier cambio en la matriz en sí no se refleja en el bucle foreach. Más detalles en el enlace.

Explicación (cita de php.net):

La primera forma recorre la matriz proporcionada por expresión_matriz. En cada iteración, el valor del elemento actual se asigna a $value y el puntero de la matriz interna avanza uno (por lo que en la siguiente 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 que después de agregar un nuevo elemento, foreach termina 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 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 foreach hace toda la lógica antes de llamar 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á esta salida:

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

Lo que significa que aceptó la modificación y la pasó porque fue modificada "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);
?>

Esto recibirás:

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

Lo que significa que la matriz se modificó, pero dado que la modificamos cuando el foreach ya estaba en el último elemento de la matriz, "decidió" no repetir más, y aunque agregamos un nuevo elemento, lo agregamos "demasiado tarde" y no se repitió.

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

Respondido el 01 de junio de 18 a las 13:06

Bueno, ¿leíste el resto de la respuesta? Tiene mucho sentido que foreach decida si se repetirá en otro momento antes incluso ejecuta el código en él. - dkasipovic

No, la matriz se modifica, pero "demasiado tarde" ya que foreach ya "piensa" que está en el último elemento (que está al comienzo de la iteración) y 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 siguiente iteración. Estoy intentando preparar un caso de prueba. - dkasipovic

@AlmaDo Mira lxr.php.net/xref/PHP_TRUNK/Zend/zend_vm_def.h#4509 Siempre se establece en el siguiente puntero cuando se itera. Entonces, cuando llegue a la última iteración, se marcará como terminado (a través del puntero NULL). Cuando luego agrega una clave en la última iteración, foreach no lo notará. - bwoebi

@DKasipovic no. No hay completo y claro explicación allí (al menos por ahora, puede que me equivoque) - alma do

En realidad, parece que @AlmaDo tiene un defecto en la comprensión de su propia lógica ... Tu respuesta está bien. - bwoebi

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 valor interno
El puntero de matriz avanza en uno (por lo que en la siguiente 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);
}

$array tener un solo elemento, por lo que según la ejecución de foreach, 1 asignar a $v y 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 tienen 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 agregó $array['baz']=3; como pasar por referencia.

Respondido 15 Abr '14, 10:04

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 usa en el ciclo. La copia se descarta inmediatamente después de que finaliza 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

Entonces, la copia se crea pero el desarrollador no se da cuenta, porque no se hace referencia a la matriz original dentro del ciclo o después de que finaliza. Sin embargo, cuando intentas modificar los elementos en un bucle, encuentras que no están modificados cuando terminas:

$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 con respecto al original no puede ser un aviso, en realidad no hay cambios con respecto al 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, cuando $ item se opera por referencia, los cambios realizados en $ item se realizan en los miembros del $ set original. El uso de $ item por referencia también evita que PHP cree la copia de la matriz. Para probar esto, primero mostraremos un guión 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 hacer un bucle, 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 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 $ set original con 6 elementos, pero nunca ingresando a 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);

Da como resultado un bucle infinito. Tenga en cuenta que esto 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 quede 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 fue escrito para crear una copia de la matriz para hacer 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 tendrá problemas.

Respondido 21 Abr '17, 09:04

El bucle foreach de PHP se puede utilizar con Indexed arrays, Associative arrays y Object public variables.

En el bucle foreach, lo primero que hace php es crear una copia de la matriz sobre la que se 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

respondido 13 nov., 17:14

Object public variables es incorrecta o, en el mejor de los casos, engañosa. 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 con un objeto. - reinas mongolas cristianas

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.