Los generadores son una de las novedades más importantes y útiles introducidas por PHP 5.5. Sin embargo, los generadores también son uno de los conceptos más difíciles de entender cuando no estás acostumbrado a trabajar con ellos. Los generadores simplifican drásticamente la creación de iteradores.
"Un iterador es un objeto que permite recorrer los valores almacenados en un contenedor".
Si utilizas por ejemplo la función
range()
junto con foreach()
en tu código, en la práctica estás iterando sobre una lista de valores:foreach (range(0, 10) as $numero) { echo "Número: $numero \n"; }
Si no existiera la función
range()
deberías crear un iterador a medida capaz de recorrer una serie de números. Para crear un iterador, crea una nueva clase que extienda de la clase Iterator
de PHP. Esta clase Iterator
define los siguientes cinco métodos abstractos que debes implementar en tu clase:class Iterator extends Traversable { // devuelve el valor del elemento actual dentro de la iteración abstract public mixed current(); // devuelve la clave del elemento actual dentro de la iteración abstract public scalar key(); // mueve la iteración hasta el siguiente elemento abstract public void next(); // mueve la iteración hasta el anterior elemento abstract public void rewind(); // comprueba si la posición actual de la iteración es válida // (es decir, si el objeto iterador sigue teniendo elementos) abstract public boolean valid(); }
PHP ya incluye muchos iteradores útiles como
ArrayIterator
para recorrer arrays y objetos yRecursiveDirectoryIterator
para recorrer de forma recursiva los contenidos de un directorio. Puedes consultar la lista de todos los iteradores que incluye PHP en la página Iteradores del manual oficial de PHP.
Volviendo al ejemplo anterior, como no existe un iterador equivalente a la función
range()
, deberías crearlo a mano extendiendo de la clase Iterator
. A continuación se muestra un posible código de este iterador:class RangeIterator extends Iterator { public function __construct($primero, $ultimo, $salto) { $this->primero = $primero; $this->ultimo = $ultimo; $this->salto = $salto; $this->actual = $this->primero; } public function current() { return $this->actual; } public function key() { return $this->actual; } public function next() { $this->actual += $this->salto; } public function rewind() { $this->actual = $this->primero; } public function valid() { return ($this->actual >= $this->primero) && ($this->actual <= $this->ultimo); } }
Como demuestra claramente el código del ejemplo anterior, crear un iterador es algo aburrido y muy costoso. Los generadores son la solución a este problema, ya que tienen funcionalidades similares a los iteradores y cuestan mucho menos crearlos.
Observa el siguiente ejemplo que muestra el código de un generador equivalente a la función
range()
de PHP y por tanto, equivalente al iterador RangeIterator
creado anteriormente:function genera_numeros($primero, $ultimo, $salto = 1) { for ($numero = $primero; $numero <= $ultimo; $numero += $salto) { yield $numero; } } foreach (genera_numeros(0, 10) as $numero) { echo "Número: $numero \n"; }
Técnicamente, un generador es muy parecido a una función. La principal diferencia es que en vez de usar la palabra reservada
return
para devolver un valor, el generador utiliza la palabra reservadayield
para devolver uno a uno todos sus valores.
Si la función
genera_numero()
anterior fuese normal, debería crear un array con todos los números y después devolverlos con la instrucción return $numeros;
. Sin embargo, al utilizar yield
en vez dereturn
, la función se convierte en un generador. Cuando se ejecute, este generador devuelve un número cada vez, hasta que se cumpla la condición que hace detenerse al generador.
¿Qué sucede si la aplicación necesita 1 millón de números? Con la función normal que genera todos los números y los guarda en un array que devuelve con
return
, el consumo de memoría sería de más de 100 MB. Con el generador anterior, sólo se devuelve un número cada vez, por lo que el consumo de memoria se reduce a menos de 1 KB.
La forma en la que un generador devuelve sus resultados es la parte más difícil de entender al empezar a trabajar con generadores. Piensa que una función normal tiene que generar toda su información de una vez antes de devolverla con
return
. Los generadores, de ahí su nombre, generan la información "al vuelo" a medida que se les solicita y la van devolviendo con yield
, que es como unreturn
que se puede llamar varias veces sin salirse de la función.
Esto es posible porque cuando se llama a la función del generador, no se ejecuta su código, sino que se devuelve un objeto de la clase
Generator
. Después, el código del generador se ejecuta paso a paso dentro del foreach
.
El resultado devuelto por
yield
también puede estar compuesto por una clave y un valor, lo que es ideal para simular que se está recorriendo un array asociativo. El siguiente ejemplo muestra un generador que utiliza cURL
para realizar peticiones HTTP a las URL indicadas:function getUrls($urls){ foreach($urls as $url) { $curl = curl_init($url); yield $url => curl_exec($curl); } } foreach(getUrls(array(...)) as $url => $contenido) { // ... }
Los generadores se pueden combinar entre sí (un generador devuelve un valor con
yield
que a su vez ha obtenido a través de otro generador que también ha utilizado el yield
) y también se pueden combinar con los iteradores. El siguiente ejemplo muestra cómo combinar iteradores y generadores para recorrer todos los contenidos de un directorio y si existe algún archivo comprimido, también se muestran sus contenidos:function recorreArchivoZip($archivoComprimido) { $zip = zip_open($archivoComprimido); $elemento = zip_read($zip); while($elemento !== false) { yield $archivoComprimido."/".zip_entry_name($elemento); } zip_close($archivoComprimido); } function recorreDirectorio($archivos) { foreach($archivos as $archivo) { if(fnmatch('*.zip', $archivo)) { foreach(recorreArchivoZip($archivo) as $elemento) { yield $elemento; } } else { yield $archivo; } } } $dir = new DirectoryIterator("..."); foreach(recorreDirectorio($dir) as $archivo) { print $archivo."\n"; }
La principal limitación de los generadores que no se puede controlar su ejecución tal y como sucede en un iterador. Como los iteradores disponen de los métodos
next()
y rewind()
, puedes ir hacia adelante o hacia atrás las veces que necesites. Los generadores no disponen de ese métodorewind()
por lo que siempre se ejecutan desde el principio hasta el final y en un único sentido.
Otra limitación de los generadores derivada de su incapacidad para ir hacia atrás es que no puedes ejecutar el mismo generador más de una vez. Si quieres repetir la ejecución, tienes que clonar el generador.
Fuente: enlace
No hay comentarios:
Publicar un comentario