Generators e yield
LidoNesta aula, você aprenderá sobre generators e a palavra-chave yield no PHP, entendendo como eles permitem iterar sobre grandes conjuntos de dados de forma eficiente, economizando memória. Veremos o que são, como funcionam, casos de uso práticos e exercícios para fixar o conteúdo.
Generators são uma funcionalidade poderosa do PHP que permite criar iteradores de forma simples e eficiente, sem a necessidade de implementar a interface Iterator manualmente. A ideia central é que, em vez de gerar um array completo com todos os valores de uma vez, você pode produzir cada valor sob demanda, quando necessário. Isso é feito através da palavra-chave yield, que pausa a execução da função, retorna o valor atual e, na próxima iteração, retoma a execução de onde parou.
Essa abordagem é especialmente útil quando se trabalha com grandes volumes de dados, como linhas de um arquivo, registros de um banco de dados ou sequências numéricas extensas. Sem generators, você precisaria carregar tudo na memória, o que pode causar estouro de memória ou degradação de desempenho. Com generators, você pode processar cada item individualmente, mantendo o consumo de memória constante.
O que são
Um generator é uma função que contém a palavra-chave yield em seu corpo. Quando você chama uma função generator, ela não executa imediatamente; em vez disso, retorna um objeto da classe Generator, que implementa a interface Iterator. A execução real da função só começa quando você itera sobre esse objeto, por exemplo, usando um loop foreach.
Diferente de uma função comum, que retorna um único valor, um generator pode produzir múltiplos valores, um por vez, a cada chamada de yield. O estado da função é preservado entre as iterações, permitindo que variáveis locais mantenham seus valores enquanto a execução é pausada e retomada.
function gerarNumeros() {
for ($i = 1; $i <= 3; $i++) {
yield $i;
}
}
$generator = gerarNumeros();
foreach ($generator as $numero) {
echo $numero . PHP_EOL;
}
// Saída: 1 2 3No exemplo acima, a função gerarNumeros() é um generator. Ela produz os números de 1 a 3, mas apenas quando solicitado pelo loop foreach. A cada iteração, o yield retorna o valor atual e pausa a execução. Na próxima iteração, o loop é retomado e a função continua de onde parou.
Economia de memória
A principal vantagem dos generators é a economia de memória. Quando você cria um array com milhões de elementos, todo ele é armazenado na memória ao mesmo tempo. Com generators, cada elemento é gerado e processado na hora, e depois descartado. Isso reduz drasticamente o pico de uso de memória.
Para ilustrar, considere a geração de uma sequência de números de 1 a 1.000.000. Se você usar um array, o PHP precisará alocar espaço para um milhão de inteiros, o que pode consumir mais de 100 MB de memória (dependendo da plataforma). Com um generator, o consumo de memória é constante, pois apenas um número é mantido por vez.
// Usando array (pode causar alto consumo de memória)
$numeros = range(1, 1000000);
foreach ($numeros as $numero) {
// processa $numero
}
// Usando generator (memória constante)
function gerarNumerosAte($limite) {
for ($i = 1; $i <= $limite; $i++) {
yield $i;
}
}
foreach (gerarNumerosAte(1000000) as $numero) {
// processa $numero
}No segundo caso, a função gerarNumerosAte não cria um array, mas produz cada número sob demanda. O loop foreach consome um número por vez, e o restante não é armazenado. Isso é especialmente útil em aplicações que precisam processar grandes arquivos, logs ou dados de APIs paginadas.
yield
A palavra-chave yield é o coração dos generators. Ela tem duas funções principais: retornar um valor para o consumidor e pausar a execução da função. Existem várias formas de usar yield, incluindo com chaves associativas, com referências e até mesmo para delegar a outro generator.
Além de yield valor, você pode usar yield $chave => $valor para produzir pares chave-valor, como em um array associativo. Isso é útil quando você precisa de chaves específicas na iteração.
function gerarCores() {
yield 'vermelho' => '#FF0000';
yield 'verde' => '#00FF00';
yield 'azul' => '#0000FF';
}
foreach (gerarCores() as $nome => $hex) {
echo "$nome: $hex" . PHP_EOL;
}
// Saída:
// vermelho: #FF0000
// verde: #00FF00
// azul: #0000FFOutra característica importante é que generators podem receber valores do consumidor usando $generator->send(). Isso permite uma comunicação bidirecional, onde o consumidor envia dados de volta para o generator, que pode usá-los para influenciar a produção de valores.
function geradorComFeedback() {
$recebido = yield 'início';
yield "Recebi: $recebido";
}
$gen = geradorComFeedback();
echo $gen->current() . PHP_EOL; // início
echo $gen->send('mensagem') . PHP_EOL; // Recebi: mensagemNeste exemplo, o generator primeiro produz 'início', depois aguarda um valor enviado pelo consumidor via send(). O valor enviado é atribuído a $recebido e usado no próximo yield.
Casos de uso
Generators são ideais para situações onde você precisa iterar sobre dados que não cabem na memória ou que são gerados dinamicamente. Aqui estão alguns casos de uso comuns:
- Leitura de arquivos grandes: Processar arquivos de texto ou CSV linha por linha, sem carregar o arquivo inteiro na memória.
- Geração de sequências infinitas: Criar sequências numéricas, como números pares ou Fibonacci, que podem ser iteradas indefinidamente, mas com controle de interrupção.
- Paginação de dados: Iterar sobre resultados de APIs paginadas, buscando a próxima página somente quando necessário.
- Transformação de dados: Aplicar transformações a cada elemento de um conjunto sem criar um novo array intermediário.
// Lendo um arquivo grande linha por linha
function lerLinhas($arquivo) {
$handle = fopen($arquivo, 'r');
while (($linha = fgets($handle)) !== false) {
yield rtrim($linha, "\n");
}
fclose($handle);
}
foreach (lerLinhas('grande.txt') as $linha) {
// processa cada linha
}Outro caso é a geração de números primos, onde você pode testar divisibilidade apenas até a raiz quadrada do número, sem precisar armazenar todos os primos anteriores.
function gerarPrimos() {
$primos = [];
$numero = 2;
while (true) {
$ehPrimo = true;
foreach ($primos as $primo) {
if ($primo * $primo > $numero) {
break;
}
if ($numero % $primo === 0) {
$ehPrimo = false;
break;
}
}
if ($ehPrimo) {
$primos[] = $numero;
yield $numero;
}
$numero++;
}
}
$gerador = gerarPrimos();
foreach ($gerador as $primo) {
if ($primo > 100) break;
echo $primo . ' ';
}
// Saída: 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97Boas práticas e observações
Ao usar generators, é importante entender que eles são de uso único: após a iteração completa, o generator não pode ser reiniciado. Se precisar iterar novamente, você deve chamar a função generator novamente. Além disso, generators não podem ser usados com return para retornar um valor final, mas você pode usar return para encerrar a função (o valor de retorno pode ser obtido via getReturn()).
Outra boa prática é usar generators em conjunto com funções de array, como iterator_to_array(), quando você precisar de um array completo, mas apenas em casos onde o tamanho é gerenciável. Para grandes volumes, prefira sempre a iteração direta.
Exercícios
- Exercício 1: Crie um generator chamado
gerarParesque produza números pares a partir de 0 até um limite passado como argumento. Teste com um limite de 10. - Exercício 2: Escreva um generator que leia um arquivo CSV e produza cada linha como um array de campos. Assuma que o arquivo tem cabeçalho e use a primeira linha como chaves associativas.
- Exercício 3: Implemente um generator que retorne os números da sequência de Fibonacci. Use-o para imprimir os primeiros 10 números.
- Exercício 4: Crie um generator que produza chaves e valores de um array associativo, mas invertendo a ordem (valor como chave e chave como valor). Use o array
['a' => 1, 'b' => 2, 'c' => 3]. - Exercício 5: Escreva um generator que receba valores enviados pelo consumidor e os acumule, retornando a soma a cada novo valor. Use
send()para enviar os valores 1, 2, 3 e 4 e imprima as somas parciais.
function gerarPares($limite) {
for ($i = 0; $i <= $limite; $i += 2) {
yield $i;
}
}
foreach (gerarPares(10) as $numero) {
echo $numero . ' ';
}
// Saída: 0 2 4 6 8 10function lerCsv($arquivo) {
$handle = fopen($arquivo, 'r');
$cabecalho = fgetcsv($handle);
while (($linha = fgetcsv($handle)) !== false) {
yield array_combine($cabecalho, $linha);
}
fclose($handle);
}
foreach (lerCsv('dados.csv') as $registro) {
// $registro é um array associativo
}function fibonacci() {
$a = 0;
$b = 1;
while (true) {
yield $a;
[$a, $b] = [$b, $a + $b];
}
}
$gen = fibonacci();
for ($i = 0; $i < 10; $i++) {
echo $gen->current() . ' ';
$gen->next();
}
// Saída: 0 1 1 2 3 5 8 13 21 34function inverterArray($array) {
foreach ($array as $chave => $valor) {
yield $valor => $chave;
}
}
$original = ['a' => 1, 'b' => 2, 'c' => 3];
foreach (inverterArray($original) as $valor => $chave) {
echo "$valor => $chave" . PHP_EOL;
}
// Saída:
// 1 => a
// 2 => b
// 3 => cfunction acumulador() {
$soma = 0;
while (true) {
$valor = yield $soma;
$soma += $valor;
}
}
$gen = acumulador();
echo $gen->current() . PHP_EOL; // 0
foreach ([1, 2, 3, 4] as $valor) {
echo $gen->send($valor) . PHP_EOL;
}
// Saída:
// 0
// 1
// 3
// 6
// 10