foreach
admite iteración sobre tres tipos diferentes de valores:
A continuación, intentaré explicar con precisión cómo funciona la iteración en diferentes casos. Con mucho, el caso más simple son los Traversable
objetos, ya que para estos foreach
es esencialmente solo el azúcar de sintaxis para el código a lo largo de estas líneas:
foreach ($it as $k => $v) { /* ... */ }
/* translates to: */
if ($it instanceof IteratorAggregate) {
$it = $it->getIterator();
}
for ($it->rewind(); $it->valid(); $it->next()) {
$v = $it->current();
$k = $it->key();
/* ... */
}
Para las clases internas, las llamadas a métodos reales se evitan mediante el uso de una API interna que esencialmente solo refleja la 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 así sort
). Esto se opone a iterar por el orden natural de las teclas (cómo funcionan a menudo las listas en otros idiomas) o no tener un orden definido en absoluto (cómo funcionan a menudo los diccionarios en otros idiomas).
Lo mismo también se aplica a los objetos, ya que las propiedades de los objetos se pueden ver como otros nombres de propiedades de mapeo de diccionario (ordenados) a sus valores, más un poco de manejo de visibilidad. En la mayoría de los casos, las propiedades del objeto no se almacenan de esta manera bastante ineficiente. Sin embargo, si comienza a iterar sobre un objeto, la representación empaquetada que se usa normalmente se convertirá en un diccionario real. En ese punto, la iteración de objetos simples se vuelve muy similar a la iteración de matrices (por lo que no estoy discutiendo mucho sobre la iteración de objetos simples aquí).
Hasta aquí todo bien. Iterar sobre un diccionario no puede ser demasiado difícil, ¿verdad? Los problemas comienzan cuando te das cuenta de que una matriz / objeto puede cambiar durante la iteración. Hay varias formas en que esto puede suceder:
- Si itera por referencia usando,
foreach ($arr as &$v)
entonces $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 era una referencia de antemano:
$ref =& $arr; foreach ($ref as $v)
- Los objetos tienen una semántica pasajera, que para la mayoría de los propósitos prácticos significa que se comportan como referencias. Por lo tanto, los objetos siempre se pueden cambiar durante la iteración.
El problema con permitir modificaciones durante la iteración es el caso en el que se elimina el elemento en el que se encuentra actualmente. Supongamos que usa un puntero para realizar un seguimiento del elemento de matriz en el que se encuentra actualmente. Si este elemento ahora está liberado, le queda un puntero colgante (que generalmente resulta en una falla predeterminada).
Hay diferentes formas de resolver este problema. PHP 5 y PHP 7 difieren significativamente en este aspecto y describiré ambos comportamientos a continuación. El resumen es que el enfoque de PHP 5 fue bastante tonto y condujo a todo tipo de problemas extraños, mientras que el enfoque más complicado de PHP 7 da como resultado un comportamiento más predecible y consistente.
Como último aspecto preliminar, debe tenerse en cuenta que PHP utiliza el recuento de referencias y la copia en escritura para administrar la memoria. Esto significa que si "copia" un valor, en realidad solo reutiliza el valor anterior e incrementa su recuento de referencia (recuento). Solo una vez que realice algún tipo de modificación, se realizará una copia real (llamada "duplicación"). Vea Le están mintiendo para una introducción más extensa sobre este tema.
PHP 5
Puntero de matriz interna y HashPointer
Las matrices en PHP 5 tienen un "puntero de matriz interno" (IAP) dedicado, que admite modificaciones correctamente: cada vez que se elimina un elemento, se comprobará si el IAP apunta a este elemento. Si lo hace, se avanza al siguiente elemento en su lugar.
Si bien 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 ejecutar el cuerpo del bucle, realizará foreach
una copia de seguridad del puntero al elemento actual y su hash en un foreach HashPointer
. Después de que se ejecute el cuerpo del bucle, el IAP se restablecerá en este elemento si aún existe. Sin embargo, si el elemento se ha eliminado, solo lo usaremos donde sea que esté actualmente el IAP. Este esquema funciona principalmente, pero hay un montón de comportamientos extraños que puedes obtener, algunos de los cuales demostraré a continuación.
Duplicación de matriz
El IAP es una característica visible de una matriz (expuesta a través de la current
familia de funciones), ya que tales cambios en el IAP cuentan como modificaciones bajo la semántica de copiar 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:
- La matriz no es una referencia (is_ref = 0). Si se trata de una referencia, se supone que los cambios se propaguen, por lo que no debe duplicarse.
- 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 refcount
se incrementará (*). Además, si foreach
se usa por referencia, la matriz (potencialmente duplicada) se convertirá en una referencia.
Considere este código como un ejemplo donde se produce la duplicación:
function iterate($arr) {
foreach ($arr as $v) {}
}
$outerArr = [0, 1, 2, 3, 4];
iterate($outerArr);
Aquí, $arr
se duplicará para evitar que los cambios de IAP se $arr
filtren $outerArr
. En términos de las condiciones anteriores, la matriz no es una referencia (is_ref = 0) y se usa en dos lugares (refcount = 2). Este requisito es desafortunado y un artefacto de la implementación subóptima (no hay ninguna preocupación de modificación durante la iteración aquí, por lo que realmente no necesitamos usar el IAP en primer lugar).
(*) Incrementar el refcount
aquí suena inocuo, pero viola la semántica de copia en escritura (COW): Esto significa que vamos a modificar el IAP de una matriz refcount = 2, mientras que COW dicta que las modificaciones solo se pueden realizar en refcount = 1 valores. Esta violación da como resultado un cambio de comportamiento visible para el usuario (mientras que un COW es normalmente transparente) porque el cambio de IAP en la matriz iterada será observable, pero solo hasta la primera modificación no IAP en la matriz. En cambio, las tres opciones "válidas" hubieran sido a) duplicar siempre, b) no incrementen refcount
y, por lo tanto, permitan que la matriz iterada se modifique arbitrariamente en el bucle o c) no utilicen el IAP (el PHP 7 solución).
Orden de avance de posición
Hay un último detalle de implementación que debe tener en cuenta para comprender correctamente los ejemplos de código a continuación. La forma "normal" de recorrer alguna estructura de datos se vería así en pseudocódigo:
reset(arr);
while (get_current_data(arr, &data) == SUCCESS) {
code();
move_forward(arr);
}
Sin embargo foreach
, al ser un copo de nieve bastante especial, elige hacer las cosas de manera ligeramente diferente:
reset(arr);
while (get_current_data(arr, &data) == SUCCESS) {
move_forward(arr);
code();
}
A saber, el puntero de la matriz ya se mueve hacia adelante antes de que se ejecute el cuerpo del bucle. Esto significa que mientras el cuerpo del bucle está trabajando en el elemento $i
, el IAP ya está en el elemento $i+1
. Esta es la razón por la cual las muestras de código que muestran modificaciones durante la iteración siempre serán unset
el siguiente elemento, en lugar del actual.
Ejemplos: sus casos de prueba
Los tres aspectos descritos anteriormente deberían proporcionarle una impresión mayormente completa de las idiosincrasias de la 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 se duplicará por foreach
: Solo refcount
se incrementa el. Cuando el cuerpo del bucle posteriormente modifica la matriz (que tiene refcount = 2 en ese punto), la duplicación ocurrirá en ese punto. Foreach continuará trabajando en una copia no modificada de $array
.
En el caso de prueba 3, una vez más la matriz no está duplicada, por foreach
lo tanto , se modificará el IAP de la $array
variable. Al final de la iteración, el IAP es NULL (lo que significa que la iteración ha terminado), lo que each
indica al regresar false
.
En los casos de prueba 4 y 5 tanto each
y reset
son funciones por referencia. El $array
tiene una refcount=2
cuando se pasa a ellos, así que tiene que ser duplicado. Como tal foreach
, trabajará 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
bucle. Considere este ejemplo:
foreach ($array as $val) {
var_dump(current($array));
}
/* Output: 2 2 2 2 2 */
Aquí debe saber que current()
es una función by-ref (en realidad: prefer-ref), a pesar de que no modifica la matriz. Tiene que ser para jugar bien con todas las otras funciones como las next
que son todas por ref. By-referencia de pasada implica que la matriz tiene que ser separado y por lo tanto $array
y el foreach-array
será diferente. La razón que obtienes en 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 avanzó el puntero al segundo.
Ahora intentemos una pequeña modificación:
$ref = &$array;
foreach ($array as $val) {
var_dump(current($array));
}
/* Output: 2 3 4 5 false */
Aquí tenemos el caso is_ref = 1, por lo que la matriz no se copia (al igual que arriba). Pero ahora que es una referencia, la matriz ya no tiene que duplicarse al pasar a la función by-ref current()
. Así current()
y foreach
trabajar en la misma matriz. Sin embargo, todavía ve el comportamiento off-by-one, debido a la forma en que foreach
avanza el puntero.
Obtiene el mismo comportamiento cuando realiza la iteración by-ref:
foreach ($array as &$val) {
var_dump(current($array));
}
/* Output: 2 3 4 5 false */
Aquí la parte importante es que foreach creará $array
un is_ref = 1 cuando se itera por referencia, por lo que básicamente tiene la misma situación que la anterior.
Otra pequeña variación, esta vez asignaremos la matriz a otra variable:
$foo = $array;
foreach ($array as $val) {
var_dump(current($array));
}
/* Output: 1 1 1 1 1 */
Aquí el recuento de la $array
es 2 cuando se inicia el ciclo, por lo 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 donde estaba antes del bucle (en este caso, estaba en la primera posición).
Ejemplos: modificación durante la iteración
Intentar dar cuenta de las modificaciones durante la iteración es donde se originaron todos nuestros problemas foreach, por lo que sirve considerar algunos ejemplos para este caso.
Considere estos bucles anidados sobre la misma matriz (donde se usa la iteración por referencia para asegurarse de que realmente sea la misma):
foreach ($array as &$v1) {
foreach ($array as &$v2) {
if ($v1 == 1 && $v2 == 1) {
unset($array[1]);
}
echo "($v1, $v2)\n";
}
}
// Output: (1, 1) (1, 3) (1, 4) (1, 5)
La parte esperada aquí es que (1, 2)
falta en la salida porque 1
se eliminó el elemento . Lo que probablemente sea inesperado es que el bucle externo se detiene después del primer elemento. ¿Porqué es eso?
La razón detrás de esto es el hack de bucle anidado descrito anteriormente: antes de que se ejecute el cuerpo del bucle, la posición IAP actual y el hash se respaldan en a HashPointer
. Después del cuerpo del bucle, se restaurará, pero solo si el elemento aún existe, de lo contrario, se usa la posición IAP actual (cualquiera que sea). En el ejemplo anterior, este es exactamente el caso: el elemento actual del bucle externo se ha eliminado, por lo que utilizará el IAP, que ya ha sido marcado como terminado por el bucle interno.
Otra consecuencia del HashPointer
mecanismo de copia de seguridad + restauración es que los cambios en el IAP a través de reset()
etc. generalmente no afectan foreach
. Por ejemplo, el siguiente código se ejecuta como si reset()
no estuviera presente:
$array = [1, 2, 3, 4, 5];
foreach ($array as &$value) {
var_dump($value);
reset($array);
}
// output: 1, 2, 3, 4, 5
La razón es que, si bien reset()
modifica temporalmente el IAP, se restaurará al elemento foreach actual después del cuerpo del bucle. Para forzar reset()
un efecto en el bucle, debe eliminar adicionalmente el elemento actual, de modo que el mecanismo de copia de seguridad / restauración falle:
$array = [1, 2, 3, 4, 5];
$ref =& $array;
foreach ($array as $value) {
var_dump($value);
unset($array[1]);
reset($array);
}
// output: 1, 1, 3, 4, 5
Pero, esos ejemplos aún son sanos. La verdadera diversión comienza si recuerda que la HashPointer
restauración usa un puntero al elemento y su hash para determinar si todavía existe. Pero: los hashes tienen colisiones, y los punteros se pueden reutilizar. Esto significa que, con una elección cuidadosa de las claves de la matriz, podemos hacer foreach
creer que todavía existe un elemento que se ha eliminado, por lo que saltará directamente a él. Un ejemplo:
$array = ['EzEz' => 1, 'EzFY' => 2, 'FYEz' => 3];
$ref =& $array;
foreach ($array as $value) {
unset($array['EzFY']);
$array['FYFY'] = 4;
reset($array);
var_dump($value);
}
// output: 1, 4
Aquí normalmente deberíamos esperar la salida de 1, 1, 3, 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. Entonces foreach termina saltando directamente al elemento recién insertado, acortando así el bucle.
Sustituyendo la entidad iterada durante el ciclo
Un último caso extraño que me gustaría mencionar, es que PHP le permite sustituir la entidad iterada durante el ciclo. Entonces puede comenzar a iterar en una matriz y luego reemplazarla con otra matriz a mitad de camino. O comience a iterar en una matriz y luego reemplácela con un objeto:
$arr = [1, 2, 3, 4, 5];
$obj = (object) [6, 7, 8, 9, 10];
$ref =& $arr;
foreach ($ref as $val) {
echo "$val\n";
if ($val == 3) {
$ref = $obj;
}
}
/* Output: 1 2 3 6 7 8 9 10 */
Como puede ver en este caso, PHP solo comenzará a iterar la otra entidad desde el principio una vez que haya ocurrido la sustitución.
PHP 7
Iteradores de tabla hash
Si aún recuerda, el principal problema con la iteración de matriz era cómo manejar la eliminación de elementos a mitad de la iteración. PHP 5 usó un único puntero de matriz interna (IAP) para este propósito, que era algo subóptimo, ya que un puntero de matriz tenía que estirarse para soportar múltiples bucles foreach simultáneos e interacción con reset()
etc., además de eso.
PHP 7 utiliza un enfoque diferente, es decir, admite la creación de una cantidad arbitraria de iteradores de tabla hash seguros y externos. Estos iteradores deben registrarse en la matriz, a partir de ese punto tienen la misma semántica que el IAP: si se elimina un elemento de la matriz, todos los iteradores de tabla hash que apuntan a ese elemento avanzarán al siguiente elemento.
Esto significa que foreach
ya no se vayan a utilizar el IAP en absoluto . El foreach
ciclo no tendrá ningún efecto en los resultados de current()
etc. y su propio comportamiento nunca estará influenciado por funciones como reset()
etc.
Duplicación de matriz
Otro cambio importante entre PHP 5 y PHP 7 se relaciona con la duplicación de matrices. Ahora que el IAP ya no se usa, la iteración de matriz por valor solo hará un refcount
incremento (en lugar de duplicar la matriz) en todos los casos. Si la matriz se modifica durante el foreach
ciclo, en ese momento 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, a saber, el caso en el que la matriz era una referencia previa:
$array = [1, 2, 3, 4, 5];
$ref = &$array;
foreach ($array as $val) {
var_dump($val);
$array[2] = 0;
}
/* Old output: 1, 2, 0, 4, 5 */
/* New output: 1, 2, 3, 4, 5 */
Anteriormente, la iteración por valor de las matrices de referencia era casos especiales. En este caso, no se produjo duplicación, por lo que todas las modificaciones de la matriz durante la iteración se reflejarían en el bucle. En PHP 7, este caso especial desapareció: una iteración por valor de una matriz siempre seguirá trabajando en los elementos originales, sin tener en cuenta las modificaciones durante el ciclo.
Esto, por supuesto, no se aplica a la iteración por referencia. Si itera por referencia, todas las modificaciones se reflejarán en el bucle. Curiosamente, lo mismo es cierto para la iteración por valor de objetos simples:
$obj = new stdClass;
$obj->foo = 1;
$obj->bar = 2;
foreach ($obj as $val) {
var_dump($val);
$obj->bar = 42;
}
/* Old and new output: 1, 42 */
Esto refleja la semántica de los objetos (por ejemplo, se comportan como referencias incluso en contextos de valor).
Ejemplos
Consideremos algunos ejemplos, comenzando con sus casos de prueba:
Los casos de prueba 1 y 2 conservan el mismo resultado: la iteración de matriz de valores siempre funciona en los elementos originales. (En este caso, el refcounting
comportamiento uniforme y 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 each()
lo que no se ve afectado por el bucle. Tendrá la misma salida antes y después.
Los casos de prueba 4 y 5 permanecen igual: each()
y reset()
duplicarán la matriz antes de cambiar el IAP, mientras que foreach
todavía usa la matriz original. (No es que el cambio de IAP hubiera importado, incluso si se hubiera compartido la matriz).
El segundo conjunto de ejemplos estaba relacionado con el comportamiento de current()
diferentes reference/refcounting
configuraciones. Esto ya no tiene sentido, ya que current()
el ciclo no lo afecta por completo, por lo que su valor de retorno siempre permanece igual.
Sin embargo, obtenemos algunos cambios interesantes cuando consideramos modificaciones durante la iteración. Espero que encuentres el nuevo comportamiento más sano. El primer ejemplo:
$array = [1, 2, 3, 4, 5];
foreach ($array as &$v1) {
foreach ($array as &$v2) {
if ($v1 == 1 && $v2 == 1) {
unset($array[1]);
}
echo "($v1, $v2)\n";
}
}
// Old output: (1, 1) (1, 3) (1, 4) (1, 5)
// New output: (1, 1) (1, 3) (1, 4) (1, 5)
// (3, 1) (3, 3) (3, 4) (3, 5)
// (4, 1) (4, 3) (4, 4) (4, 5)
// (5, 1) (5, 3) (5, 4) (5, 5)
Como puede ver, el bucle externo ya no se cancela después de la primera iteración. La razón es que ambos bucles ahora tienen iteradores de tabla hash completamente separados, y ya no hay contaminación cruzada de ambos bucles a través de un IAP compartido.
Otro caso de borde extraño que se soluciona ahora, es el efecto extraño que obtienes al eliminar y agregar elementos que tienen el mismo hash:
$array = ['EzEz' => 1, 'EzFY' => 2, 'FYEz' => 3];
foreach ($array as &$value) {
unset($array['EzFY']);
$array['FYFY'] = 4;
var_dump($value);
}
// Old output: 1, 4
// New output: 1, 3, 4
Anteriormente, el mecanismo de restauración de HashPointer saltaba directamente al nuevo elemento porque "parecía" que era lo mismo que el elemento eliminado (debido al hash y al puntero colisionantes). Como ya no confiamos en el elemento hash para nada, esto ya no es un problema.