Un générateur est une fonction qui contient yield. L'appeler n'exécute pas le corps ; elle renvoie un objet Generator que foreach peut parcourir. Chaque yield $value rend une valeur à la boucle et met la fonction en pause jusqu'à ce que la boucle demande la suivante, si bien que les valeurs sont produites une par une au lieu d'être d'abord rassemblées dans un tableau.
La sortie alterne entre le générateur et la boucle : chaque valeur n'est produite que quand la boucle la demande.
Les générateurs économisent la mémoire
range(1, 1000000) construit un tableau d'un million d'entiers avant que la boucle commence. Un générateur qui rend les mêmes nombres n'en garde jamais plus d'un. Exécutez-le et comparez la mémoire dont chaque version a besoin :
Les deux boucles affichent la même somme, 500000500000, mais la version tableau indique extra memory 16.0 MB et le générateur extra memory 0.0 MB. L'écart grandit avec les données. C'est la même idée qui rend les générateurs utiles pour les gros fichiers, les résultats de base de données et les pages d'API : traiter un élément, l'abandonner, prendre le suivant.
Rendre des clés et des valeurs
yield $key => $value définit la clé que reçoit foreach ($gen as $key => $value). Sans clé, un générateur numérote ses valeurs depuis 0, comme une liste. Un usage courant est la lecture d'un fichier ligne par ligne avec les numéros de ligne :
Une seule ligne est en mémoire à la fois, donc la même fonction gère un fichier de log de 2 Go. fopen() et fgets() présentent les fonctions de fichier utilisées ici.
Séquences infinies et chaînes de traitement paresseuses
Un générateur peut boucler indéfiniment, car il ne s'exécute que dans la mesure où le consommateur le demande. Arrêtez avec break, ou entourez-le d'un autre générateur qui prend les premières valeurs. Enchaîner de petits générateurs donne une chaîne de traitement où chaque étape est paresseuse :
fibonacci() ne s'arrête jamais d'elle-même, et pourtant le programme se termine : take() cesse de demander après le nombre voulu, et rien au-delà n'est jamais calculé.
yield from : déléguer à un autre générateur
yield from transmet chaque valeur d'un tableau, d'un itérateur ou d'un autre générateur. C'est ainsi qu'on découpe un générateur en plus petits, ou qu'on parcourt récursivement une structure imbriquée :
yield from garde les clés intérieures, donc les deux tableaux rendent les clés 0 et 1. iterator_to_array() écrase alors a et b. Passez false en second argument dès que seules les valeurs comptent.
send() et getReturn()
Un générateur peut aussi recevoir des valeurs. $x = yield; fait une pause et attend ; $gen->send($value) reprend le générateur avec $value comme résultat de ce yield. Et un générateur peut faire return d'une valeur finale, que vous lisez avec getReturn() une fois qu'il est terminé.
La plupart du code n'a jamais besoin de send() ; la lecture paresseuse avec foreach est l'usage quotidien. C'est la brique de base des bibliothèques de coroutines.
Un générateur ne se parcourt qu'une fois
Les générateurs avancent et ne peuvent pas être rembobinés. Une fois qu'un foreach en a consommé un, une seconde boucle sur le même objet lève une exception :
La solution est d'appeler de nouveau la fonction génératrice à chaque passage, foreach (colors() as $c), ou de rassembler les valeurs une fois avec iterator_to_array(colors(), false) quand vous en avez vraiment besoin deux fois. Si vous le faites souvent, un simple tableau était le bon choix pour ces données.
Questions fréquentes
Que fait yield en PHP ?
yield $value rend une valeur au code qui parcourt la fonction et met la fonction en pause à cet endroit. L'itération suivante de la boucle la reprend après le yield. Toute fonction contenant yield renvoie un objet Generator au lieu de s'exécuter quand vous l'appelez.
Pourquoi utiliser un générateur plutôt qu'un tableau ?
Un tableau garde tous ses éléments en mémoire en même temps ; un générateur produit un élément à la fois. Parcourir range(1, 1000000) alloue le million d'entiers, alors qu'un générateur qui les rend avec yield garde une mémoire stable. Les générateurs permettent aussi des séquences infinies et arrêtent le travail tôt quand la boucle s'interrompt.
Qu'est-ce que yield from en PHP ?
yield from $source transmet chaque valeur d'un autre générateur, d'un tableau ou d'un Traversable, comme si le générateur courant les avait lui-même rendues. L'expression vaut aussi la valeur de retour du générateur intérieur.
Peut-on parcourir un générateur deux fois ?
Non. Un générateur avance uniquement. Une fois un foreach terminé, boucler à nouveau lève Exception: Cannot traverse an already closed generator. Appelez de nouveau la fonction génératrice pour obtenir un nouveau générateur, ou stockez les valeurs avec iterator_to_array() si vous en avez besoin deux fois.
Comment convertir un générateur en tableau ?
iterator_to_array($gen) rassemble toutes les valeurs en gardant les clés du générateur. Si les clés se répètent, par exemple après yield from, les valeurs suivantes écrasent les précédentes, passez donc false en second argument pour obtenir une simple liste : iterator_to_array($gen, false).