O que significa yield em PHP?

232

Recentemente, deparei-me com este código:

function xrange($min, $max) 
{
    for ($i = $min; $i <= $max; $i++) {
        yield $i;
    }
}

Eu nunca vi essa yieldpalavra - chave antes. Tentando executar o código que recebo

Erro de análise: erro de sintaxe, T_VARIABLE inesperado na linha x

Então, qual é essa yieldpalavra - chave? É mesmo válido PHP? E se for, como eu o uso?

Gordon
fonte

Respostas:

355

O que é yield?

A yieldpalavra-chave retorna dados de uma função geradora:

O coração de uma função geradora é a palavra-chave yield. Em sua forma mais simples, uma declaração de rendimento se parece muito com uma declaração de retorno, exceto que, em vez de interromper a execução da função e retornar, o rendimento fornece um valor ao código que circula sobre o gerador e pausa a execução da função do gerador.

O que é uma função de gerador?

Uma função de gerador é efetivamente uma maneira mais compacta e eficiente de escrever um iterador . Ele permite que você defina uma função (sua xrange) que calculará e retornará valores enquanto você estiver fazendo um loop sobre ela :

foreach (xrange(1, 10) as $key => $value) {
    echo "$key => $value", PHP_EOL;
}

Isso criaria a seguinte saída:

0 => 1
1 => 2

9 => 10

Você também pode controlar o $keyno foreachusando

yield $someKey => $someValue;

Na função do gerador, $someKeyé o que você deseja que apareça $keye $someValueseja o valor em $val. No exemplo da pergunta é isso $i.

Qual é a diferença para funções normais?

Agora você pode se perguntar por que não estamos simplesmente usando a rangefunção nativa do PHP para obter essa saída. E você está certo. A saída seria a mesma. A diferença é como chegamos lá.

Quando usamos rangePHP, irá executá-lo, criar toda a matriz de números na memória e returnque toda variedade ao foreachcircuito que irá, em seguida, passar por isso e fornecer os valores. Em outras palavras, o foreachirá operar no próprio array. A rangefunção e a foreachúnica "conversa" uma vez. Pense nisso como receber um pacote pelo correio. O entregador entregará o pacote e sairá. E então você desembrulha o pacote inteiro, retirando o que estiver lá.

Quando usamos a função de gerador, o PHP entra na função e a executa até encontrar o final ou uma yieldpalavra - chave. Quando encontrar a yield, retornará o valor naquele momento para o loop externo. Em seguida, ele volta para a função de gerador e continua a partir de onde produziu. Como você xrangemantém um forloop, ele será executado e renderá até que $maxseja atingido. Pense nisso como foreacho gerador jogando pingue-pongue.

Por que eu preciso disso?

Obviamente, os geradores podem ser usados ​​para solucionar os limites de memória. Dependendo do seu ambiente, executar um range(1, 1000000)script fatal será um passo, enquanto o mesmo com um gerador funcionará bem. Ou como a Wikipedia coloca:

Como os geradores calculam seus valores gerados somente sob demanda, eles são úteis para representar sequências que seriam caras ou impossíveis de calcular de uma só vez. Isso inclui, por exemplo, sequências infinitas e fluxos de dados ao vivo.

Os geradores também devem ser bem rápidos. Mas lembre-se de que, quando estamos falando rápido, geralmente estamos falando em números muito pequenos. Portanto, antes que você corra e mude todo o seu código para usar geradores, faça uma referência para ver onde faz sentido.

Outro caso de uso para geradores são as rotinas assíncronas. A yieldpalavra-chave não apenas retorna valores, mas também os aceita. Para detalhes sobre isso, veja as duas excelentes postagens no blog abaixo.

Desde quando posso usar yield?

Geradores foram introduzidos no PHP 5.5 . Tentar usar yieldantes dessa versão resultará em vários erros de análise, dependendo do código que segue a palavra-chave. Portanto, se você receber um erro de análise desse código, atualize seu PHP.

Fontes e leituras adicionais:

Gordon
fonte
1
Por favor explique sobre o que os benefícios de yeildmais de, digamos, uma solução como esta: ideone.com/xgqevM
Mike
1
Ah, bem, e os avisos que eu estava gerando. Hã. Bem, experimentei emular o Generators for PHP> = 5.0.0 com uma classe auxiliar e, sim, um pouco menos legível, mas posso usar isso no futuro. Tópico interessante. Obrigado!
27515 Mike
Não legibilidade, mas uso de memória! Compare memória usada para a iteração sobre return range(1,100000000)e for ($i=0; $i<100000000; $i++) yield $i
Emix
@ Mike sim, isso já está explicado na minha resposta. No exemplo do outro Mike, a memória dificilmente é um problema, porque ele está apenas repetindo 10 valores.
Gordon
1
@ Mike Um problema com o xrange é que o uso de limites estáticos é útil para aninhar, por exemplo (por exemplo, pesquisando sobre uma variedade n dimensional ou uma seleção rápida recursiva usando geradores, por exemplo). Você não pode aninhar loops de intervalo de x porque há apenas uma única instância de seu contador. A versão Yield não sofre esse problema.
Shayne
43

Esta função está usando yield:

function a($items) {
    foreach ($items as $item) {
        yield $item + 1;
    }
}

é quase o mesmo que este sem:

function b($items) {
    $result = [];
    foreach ($items as $item) {
        $result[] = $item + 1;
    }
    return $result;
}

A única diferença é que a()retorna um gerador e b()apenas uma matriz simples. Você pode iterar em ambos.

Além disso, o primeiro não aloca uma matriz completa e, portanto, requer menos memória.

tsusanka
fonte
2
addt notes dos documentos oficiais: No PHP 5, um gerador não pôde retornar um valor: isso resultaria em um erro de compilação. Uma declaração de retorno vazia era uma sintaxe válida dentro de um gerador e encerraria o gerador. Desde o PHP 7.0, um gerador pode retornar valores, que podem ser recuperados usando Generator :: getReturn (). php.net/manual/en/language.generators.syntax.php
Programador Dancuk
Simples e conciso.
John Miller
24

exemplo simples

<?php
echo '#start main# ';
function a(){
    echo '{start[';
    for($i=1; $i<=9; $i++)
        yield $i;
    echo ']end} ';
}
foreach(a() as $v)
    echo $v.',';
echo '#end main#';
?>

resultado

#start main# {start[1,2,3,4,5,6,7,8,9,]end} #end main#

exemplo avançado

<?php
echo '#start main# ';
function a(){
    echo '{start[';
    for($i=1; $i<=9; $i++)
        yield $i;
    echo ']end} ';
}
foreach(a() as $k => $v){
    if($k === 5)
        break;
    echo $k.'=>'.$v.',';
}
echo '#end main#';
?>

resultado

#start main# {start[0=>1,1=>2,2=>3,3=>4,4=>5,#end main#
Pense grande
fonte
Então, ele retorna sem interromper a função?
Lucas Bustamante
22

yieldA palavra-chave serve para definição de "geradores" no PHP 5.5. Ok, então o que é um gerador ?

Do php.net:

Os geradores fornecem uma maneira fácil de implementar iteradores simples sem a sobrecarga ou complexidade de implementar uma classe que implementa a interface do Iterator.

Um gerador permite escrever código que usa foreach para iterar sobre um conjunto de dados sem a necessidade de criar uma matriz na memória, o que pode fazer com que você exceda um limite de memória ou exigir uma quantidade considerável de tempo de processamento para gerar. Em vez disso, você pode escrever uma função de gerador, que é a mesma que uma função normal, exceto que, em vez de retornar uma vez, um gerador pode render quantas vezes for necessário para fornecer os valores a serem iterados.

Deste local: geradores = geradores, outras funções (apenas funções simples) = funções.

Portanto, eles são úteis quando:

  • você precisa fazer coisas simples (ou coisas simples);

    O gerador é realmente muito mais simples do que implementar a interface Iterator. por outro lado, é claro que os geradores são menos funcionais. compare-os .

  • você precisa gerar grandes quantidades de memória para economizar dados;

    na verdade, para economizar memória, podemos gerar os dados necessários por meio de funções para cada iteração de loop e, após a iteração, utilizar o lixo. então aqui os principais pontos são - código claro e provavelmente desempenho. veja o que é melhor para suas necessidades.

  • você precisa gerar sequência, que depende de valores intermediários;

    isso está se estendendo ao pensamento anterior. geradores podem facilitar as coisas em comparação com funções. verifique o exemplo de Fibonacci e tente fazer a sequência sem gerador. Também os geradores podem trabalhar mais rápido neste caso, pelo menos por causa do armazenamento de valores intermediários em variáveis ​​locais;

  • você precisa melhorar o desempenho.

    eles podem trabalhar mais rápido que as funções em alguns casos (consulte o benefício anterior);

QArea
fonte
1
Não entendi como os geradores funcionam. essa classe implementa a interface do iterador. pelo que sei, as classes de iteradores permitem-me configurar como quero iterar sobre um objeto. por exemplo, o ArrayIterator obtém uma matriz ou objeto para que eu possa modificar valores e chaves enquanto iteramos. Portanto, se os iteradores obtêm todo o objeto / array, como o gerador não precisa construir todo o array na memória ???
user3021621
7

Com yieldvocê, você pode descrever facilmente os pontos de interrupção entre várias tarefas em uma única função. Isso é tudo, não há nada de especial nisso.

$closure = function ($injected1, $injected2, ...){
    $returned = array();
    //task1 on $injected1
    $returned[] = $returned1;
//I need a breakpoint here!!!!!!!!!!!!!!!!!!!!!!!!!
    //task2 on $injected2
    $returned[] = $returned2;
    //...
    return $returned;
};
$returned = $closure($injected1, $injected2, ...);

Se task1 e task2 estiverem altamente relacionadas, mas você precisará de um ponto de interrupção entre eles para fazer outra coisa:

  • memória livre entre o processamento de linhas do banco de dados
  • executar outras tarefas que fornecem dependência para a próxima tarefa, mas que não são relacionadas ao entender o código atual
  • fazendo chamadas assíncronas e aguarde os resultados
  • e assim por diante ...

então os geradores são a melhor solução, porque você não precisa dividir seu código em muitos fechamentos ou misturá-lo com outro código ou usar retornos de chamada, etc. Você apenas usa yieldpara adicionar um ponto de interrupção e pode continuar a partir disso. ponto de interrupção se você estiver pronto.

Adicione ponto de interrupção sem geradores:

$closure1 = function ($injected1){
    //task1 on $injected1
    return $returned1;
};
$closure2 = function ($injected2){
    //task2 on $injected2
    return $returned1;
};
//...
$returned1 = $closure1($injected1);
//breakpoint between task1 and task2
$returned2 = $closure2($injected2);
//...

Adicionar ponto de interrupção com geradores

$closure = function (){
    $injected1 = yield;
    //task1 on $injected1
    $injected2 = (yield($returned1));
    //task2 on $injected2
    $injected3 = (yield($returned2));
    //...
    yield($returnedN);
};
$generator = $closure();
$returned1 = $generator->send($injected1);
//breakpoint between task1 and task2
$returned2 = $generator->send($injected2);
//...
$returnedN = $generator->send($injectedN);

Nota: É fácil cometer erros nos geradores; portanto, sempre escreva testes de unidade antes de implementá-los! note2: Usar geradores em um loop infinito é como escrever um fechamento com comprimento infinito ...

inf3rno
fonte
4

Nenhuma das respostas acima mostra um exemplo concreto usando matrizes massivas preenchidas por membros não numéricos. Aqui está um exemplo usando uma matriz gerada por explode()um arquivo .txt grande (262MB no meu caso de uso):

<?php

ini_set('memory_limit','1000M');

echo "Starting memory usage: " . memory_get_usage() . "<br>";

$path = './file.txt';
$content = file_get_contents($path);

foreach(explode("\n", $content) as $ex) {
    $ex = trim($ex);
}

echo "Final memory usage: " . memory_get_usage();

A saída foi:

Starting memory usage: 415160
Final memory usage: 270948256

Agora compare isso com um script semelhante, usando a yieldpalavra-chave:

<?php

ini_set('memory_limit','1000M');

echo "Starting memory usage: " . memory_get_usage() . "<br>";

function x() {
    $path = './file.txt';
    $content = file_get_contents($path);
    foreach(explode("\n", $content) as $x) {
        yield $x;
    }
}

foreach(x() as $ex) {
    $ex = trim($ex);
}

echo "Final memory usage: " . memory_get_usage();

A saída para este script foi:

Starting memory usage: 415152
Final memory usage: 415616

Claramente, a economia no uso da memória foi considerável (ΔMemoryUsage -----> ~ 270,5 MB no primeiro exemplo, ~ 450B no segundo exemplo).

David Partyka
fonte
3

Um aspecto interessante, que vale a pena discutir aqui, é cedido por referência . Toda vez que precisamos alterar um parâmetro para que seja refletido fora da função, precisamos passar esse parâmetro por referência. Para aplicar isso aos geradores, simplesmente adicionamos um e comercial &ao nome do gerador e à variável usada na iteração:

 <?php 
 /**
 * Yields by reference.
 * @param int $from
 */
function &counter($from) {
    while ($from > 0) {
        yield $from;
    }
}

foreach (counter(100) as &$value) {
    $value--;
    echo $value . '...';
}

// Output: 99...98...97...96...95...

O exemplo acima mostra como a alteração dos valores iterados no foreachloop altera a $fromvariável dentro do gerador. Isso ocorre porque $fromé produzido por referência devido ao e comercial antes do nome do gerador. Por esse motivo, a $valuevariável dentro do foreachloop é uma referência à $fromvariável dentro da função do gerador.

Bud Damyanov
fonte
0

O código abaixo ilustra como o uso de um gerador retorna um resultado antes da conclusão, ao contrário da abordagem tradicional de não gerador que retorna uma matriz completa após a iteração completa. Com o gerador abaixo, os valores são retornados quando prontos, sem a necessidade de esperar que uma matriz seja completamente preenchida:

<?php 

function sleepiterate($length) {
    for ($i=0; $i < $length; $i++) {
        sleep(2);
        yield $i;
    }
}

foreach (sleepiterate(5) as $i) {
    echo $i, PHP_EOL;
}
Risteard
fonte
Portanto, não é possível usar yield para gerar código html em php? Não conheço os benefícios em um ambiente real
Giuseppe Lodi Rizzini
@GiuseppeLodiRizzini o que faz você pensar isso?
Brad Kent