Ir al contenido principal

¿Qué tan bueno es realmente el “foreach” en PHP?

Como toda buena historia, esta comienza hace algún tiempo. El que fuera mi jefe por allá en la primera década del 2000, realmente odiaba (y mucho) el uso del foreach en el código PHP. Prefería que usáramos alguna alternativa diferente, alguna combinación del for o del while. ¿Por qué? Ve tú a saber, nunca fue abierto respecto a las razones de su aprensión hacia ese constructor propio del lenguaje. Pero antes de continuar, veamos qué es y para qué nos puede servir.

Arreglos, tenían que ser arreglos

¿Qué es foreach? De acuerdo al manual de PHP, su definición es la siguiente:

El constructor foreach proporciona un modo sencillo de iterar sobre arrays. foreach funciona sólo sobre arrays y objetos, y emitirá un error al intentar usarlo con una variable de un tipo diferente de datos o una variable no inicializada.

Para su uso correcto existen dos sintaxis validas, a saber:

foreach (expresión_array as $value) { ... }

foreach (expresión_array as $key => $value) { ... }

Como recalca su definición, el principal uso del foreach es el de recorrer arreglos (o arrays en inglés). Los arreglos son básicamente agrupaciones de valores y PHP permite muchos diferentes tipos de iteraciones con estos elementos. Tanto es así, que muchas soluciones pueden realizarse con base en arreglos sin recurrir siquiera al manejo de clases, aunque claro, si el arreglo es demasiado complejo o profundo (es decir, sus elementos contienen otros arreglos) se corre el riesgo de perderse dentro del código resultante.

Algunos ejemplos de arreglos son:

// Arreglo simple
$numeros= array(1, 2, 3, 4, 5, 6);

// Arreglo más complejo, con valores asociativos
$frutas = array (
    "frutas"  => array("a" => "naranja", "b" => "plátano", "c" => "manzana"),
    "numeros" => $numeros,
    "hoyos"   => array("primero", 5 => "segundo", "tercero")
);

// Misma definición simplificada a partir de PHP 8, usando [...]
$frutas = [
    "frutas"  => [ "a" => "naranja", "b" => "plátano", "c" => "manzana" ],
    "numeros" => $numeros,
    "hoyos"   => [ "primero", 5 => "segundo", "tercero" ]
];

Y podemos usar foreach para recorrer un arreglo como el de $frutas de la siguiente manera:

foreach ($frutas as $key => $value) {
    print_r($value);
}

El mito del rendimiento

La preocupación del rendimiento siempre estuvo latente cada que usaba foreach. Ya de por sí el uso de ciclos en el código tiene una connotación negativa ya que se asume que son “poco eficientes” en cuanto a velocidad. Por supuesto, hay de ciclos a ciclos. En PHP y otros lenguajes estructurados similares tenemos constructores alternos como el for, while y do-while. ¿Cuál es más eficiente? En Internet  existen muchos recursos disponibles sobre el tema (puedes Googlear “PHP foreach vs for” para que te hagas a una idea).

¿Será precisamente for la mejor alternativa? De los ejemplos anteriores, veamos como el arreglo $numeros puede recorrerse usando estos constructores para obtener resultados similares:

// Usando "foreach" para recorrer $numeros
foreach ($numeros as $key => $value) { 
     ...
}

// El equivalente usando "for"
$total = count($numeros);
for ($key = 0; $key < $total; $key++) {
    $value = $numeros[$key];
    ...
}

En lo personal, no me parece que el uso de for pueda resultar más rápido, pero quizás la implementación interna del constructor pueda afectar el resultado y no es visible a simple vista. Para salir de dudas, hagamos la prueba con un sencillo script:

$max = 1000 * 1000;
// Creación de un arreglo de 1000 x 1000 elementos
$numeros = range(0, $max);

// Tiempo de inicio para for()...
$t = microtime(true);
for ($key = 0; $key < $max; $key++) {
    // Salida a pantalla cada 1000 conteos
    if ($numeros[$key] % 1000 == 0) { echo '.'; }
}
// Imprime tiempo total con 3 decimales
echo "\n";
echo  number_format((microtime(true) - $t), 3);
echo "\n\n";

// Tiempo de inicio para foreach()...
$t = microtime(true);
foreach ($numeros as $key => $value) {
    // Salida a pantalla cada 1000 conteos
    if ($value % 1000 == 0) { echo '.'; }
}
// Imprime tiempo total con 3 decimales
echo "\n";
echo  number_format((microtime(true) - $t), 3);
echo "\n";

Los resultados obtenidos fueron diferentes cada vez que ejecutaba el script, pero en general y con pocas excepciones, el recorrido con foreach resulta más rápido que con for, por unos cuantos milisegundos (recuerden que estamos simplemente recorriendo un arreglo de muchos valores simples, en aplicaciones más complejas el uso de memoria puede impactar estos resultados todavía más):

For:     0.089 / 0.083 / 0.083 / 0.080 / 0.082 / 0.102 / 0.082 / 0.083
Foreach: 0.077 / 0.079 / 0.082 / 0.076 / 0.076 / 0.077 / 0.106 / 0.080

Así que en lo que a velocidad respecta, podemos decir que vamos a lo seguro con foreach.

Duplicidad de los datos

Otro de los grandes “peros” tiene que ver con que foreach “exporta” el valor de cada elemento a una variable asociada (un elemento por vez), duplicando el valor de ese elemento durante el tiempo de ejecución del ciclo. Normalmente esto no debiera ser tema de preocupación a menos que cada elemento tenga un peso considerable respecto al uso de memoria (por ejemplo, datos capturados de una tabla de comentarios en una base de datos). En este caso se puede argumentar que al usar for se puede trabajar directamente sobre el elemento del arreglo sin duplicarlo (a menos que se le asigne a una variable, con lo que tendríamos el mismo caso del foreach y no se ganaría ninguna ventaja).

Sin embargo, la defensa procede con su alegato…

Existe una forma de usar foreach sin tener que duplicar valores, indicando al constructor que acceda al elemento directamente, es decir, asignando su valor por referencia de forma que se trabaje directamente sobre el elemento original y no sobre una copia. Esto se logra de la siguiente forma:

foreach ($numeros as $key => &$value) { ... }

Al agregar el identificador “&” estamos instruyendo a PHP para que acceda al valor por referencia del elemento y evitar así duplicarlo. Igualmente, al modificar $value se estaría modificando directamente el valor en $numeros[$key], lo que puede ser un efecto colateral tanto positivo como negativo, de acuerdo a las necesidades del programa o por descuido del programador, según sea el caso.

En estos casos, hay que tener una precaución extra al terminar el ciclo:

La referencia de $value y el último elemento del arreglo permanecen aún después del bucle foreach. Se recomienda destruirlos con unset($value). De lo contrario, pueden tenerse resultados no deseados si se reutiliza la variable $value posteriormente, ya que internamente sigue vinculada al último elemento del arreglo.

foreach ($numeros as $key => &$value) { ... }
unset($value);

Por demás, todo bien. ¿Qué más hay?

La no muy mentada interfaz “Iterator”

Otro uso del foreach es el de recorrer objetos, en este caso, objetos que implementan la interfaz Iterator, usada “para iteradores externos u objetos que pueden ser iterados internamente por sí mismos” (definición tomada del manual de PHP).

Este sencillo ejemplo muestra el orden en el que se llaman a los métodos cuando se emplea un foreach con una clase del tipo Iterator.

class myIterator implements Iterator {
    // Control de posición o llave del arreglo
    private $position = 0;
    // Valores de interes
    private $array = array(
        "firstelement",
        "secondelement",
        "lastelement",
    );

    // Constructor, inicializa control de posición (llave)
    public function __construct() {
        $this->position = 0;
    }

    public function rewind(): void {
        $this->position = 0;
    }

    // Se ejecuta al recuperar el valor del elemento actual
    #[\ReturnTypeWillChange]
    public function current() {
        return $this->array[$this->position];
    }

    // Se ejecuta al solicitar la llave en uso
    #[\ReturnTypeWillChange]
    public function key() {
        return $this->position;
    }

    // Invoca siguiente elemento
    public function next(): void {
        ++$this->position;
    }

    // Valida si existe el elemento solicitado
    public function valid(): bool {
        return isset($this->array[$this->position]);
    }
}

$it = new myIterator;

foreach($it as $key => $value) {
    var_dump($key, $value);
    echo "\n";
}

Al ejecutar, se tiene una salida como la siguiente:

0 => firstelement
1 => secondelement
2 => lastelement

Este manejo permite la implementación de elementos mucho más versátiles que un simple arreglo y es por supuesto, algo que podemos explotar debidamente solamente a través de foreach.

Conclusión

En lo que a mi respecta, seguiré usando el foreach sin preocuparme tanto por su rendimiento. A estas alturas y con tantas líneas de código encima, creo que si un script responde muy lento se debe a... muchas causas y habrá que investigar cuál de tantas aplica, pero muy seguramente no será por cuenta del uso del foreach.

¿Qué hay de ti? ¿Has tenido inconvenientes de rendimiento sea por velocidad o memoria, por cuenta de usar este constructor? Si es así (o si por el contrario, estas feliz usándolo o simplemente te da igual) te invito a compartir esa experiencia en la sección de comentarios.

Hasta una próxima.

Imagen de portada provista por Pixabay

Comentarios

Entradas populares de este blog

Manejo de recursos HTML para tus páginas web con PHP

Déjame saber si te resulta familiar esta situación: páginas web que descargan el mismo recurso (sean estilos CSS o código Javascript) más de una vez o incluyen recursos remotos que tardan una eternidad en cada descarga. Yo lo he visto en más de una ocasión y no es difícil imaginar el porqué ocurre. Un desarrollador incluye el recurso de estilos que necesita su segmento de código y otro hace lo mismo, sin reparar (o sin que siquiera importe) que comparten el mismo recurso. En otro escenario muy común, acostumbran incluir muchos recursos remotos, con lo que el rendimiento de la página depende de lo rápido que responda dicho recurso. ¿Puede hacerse algo al respecto? Claro que si. Vamos a crear una clase en PHP que se encargue de administrar estos recursos y que nos facilite su despliegue en la página sin repeticiones . ¿Y respecto a la demora en la carga de recursos remotos? Atendamos una cosa por vez, porque como dicen por ahí: «Vísteme despacio, que tengo prisa». Administrando ...

Manejo de clases globales únicas en PHP

¿Cómo acceder desde cualquier script en tu proyecto a Clases y/o funciones de uso común? Este puede ser una de las primeras directrices a establecer para cualquier proyecto porque siempre, siempre , sea en  PHP  u otro lenguaje, será necesario usar recursos comunes. En PHP existen diferentes alternativas para su manejo, ya sea por medio de variables globales o de clases/objetos estáticos. A continuación consideraremos una propuesta para este manejo. Creación de recursos globales Para ilustrar esta solución, partimos de la necesidad de implementar una librería para manejo de servicios relacionados con el servidor Web, que de forma amigable nos permita disponer de información como: Valores almacenados de la variable superglobal $_SERVER de PHP. Valores asociados a la consulta realizada por el usuario, por Ej. la dirección IP del usuario o la URL ingresada. Valores asociados al servidor web usado, por Ej. la dirección IP del servidor o la ubicación del script que ej...