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
Publicar un comentario