Cosa significa resa in PHP?


232

Di recente mi sono imbattuto in questo codice:

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

Non ho mai visto questa yieldparola chiave prima d'ora. Sto cercando di eseguire il codice che ottengo

Errore di analisi: errore di sintassi, T_VARIABLE imprevisto sulla riga x

Cos'è questa yieldparola chiave? È anche valido PHP? E se lo è, come lo uso?

Risposte:


355

Che cosa è yield?

La yieldparola chiave restituisce i dati da una funzione del generatore:

Il cuore di una funzione del generatore è la parola chiave yield. Nella sua forma più semplice, un'istruzione yield assomiglia molto a un'istruzione return, tranne per il fatto che invece di interrompere l'esecuzione della funzione e restituirla, yield fornisce invece un valore al codice che scorre ciclicamente sul generatore e mette in pausa l'esecuzione della funzione generatore.

Cos'è una funzione generatore?

Una funzione di generatore è effettivamente un modo più compatto ed efficiente per scrivere un Iteratore . Ti permette di definire una funzione (la tua xrange) che calcolerà e restituirà i valori mentre ci stai passando sopra :

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

Ciò creerebbe il seguente output:

0 => 1
1 => 2

9 => 10

Puoi anche controllare $keyin foreachin usando

yield $someKey => $someValue;

Nella funzione generatore, $someKeyè quello che vuoi apparire $keyed $someValueessere il valore in $val. Nell'esempio della domanda questo è $i.

Qual è la differenza rispetto alle normali funzioni?

Ora potresti chiederti perché non stiamo semplicemente usando la rangefunzione nativa di PHP per ottenere quell'output. E hai ragione. L'output sarebbe lo stesso. La differenza è come siamo arrivati ​​lì.

Quando usiamo rangePHP, eseguirà esso, creare l'intera matrice di numeri in memoria e returnche intero array al foreachciclo che poi andare su di esso ed emettere i valori. In altre parole, foreachfunzionerà sull'array stesso. La rangefunzione e l' foreachunico "discorso" una volta. Pensalo come ottenere un pacco per posta. Il fattorino ti consegnerà il pacco e partirà. E poi scartate l'intero pacchetto, tirando fuori tutto quello che c'è dentro.

Quando usiamo la funzione generatore, PHP entrerà nella funzione e la eseguirà fino a quando non incontra la fine o una yieldparola chiave. Quando incontra a yield, restituirà quindi qualunque sia il valore in quel momento nel ciclo esterno. Quindi ritorna nella funzione generatore e continua da dove ha ceduto. Dal momento che il tuo xrangedetiene un forloop, verrà eseguito e cederà fino al $maxraggiungimento. Pensalo come il foreache il generatore che gioca a ping pong.

Perché ne ho bisogno?

Ovviamente, i generatori possono essere utilizzati per aggirare i limiti di memoria. A seconda del tuo ambiente, fare una range(1, 1000000)volontà fatale lo script mentre lo stesso con un generatore funzionerà bene. O come dice Wikipedia:

Poiché i generatori calcolano i loro valori ottenuti solo su richiesta, sono utili per rappresentare sequenze che sarebbero costose o impossibili da calcolare in una sola volta. Questi includono ad esempio sequenze infinite e flussi di dati live.

I generatori dovrebbero anche essere piuttosto veloci. Ma tieni presente che quando parliamo velocemente, di solito parliamo di numeri molto piccoli. Quindi, prima di scappare e cambiare tutto il codice per utilizzare i generatori, fai un benchmark per vedere dove ha senso.

Un altro caso d'uso per i generatori sono le coroutine asincrone. La yieldparola chiave non solo restituisce valori, ma li accetta anche. Per i dettagli, consultare i due eccellenti post sul blog collegati di seguito.

Da quando posso usare yield?

I generatori sono stati introdotti in PHP 5.5 . Cercare di utilizzare yieldprima di quella versione comporterà vari errori di analisi, a seconda del codice che segue la parola chiave. Quindi, se ricevi un errore di analisi da quel codice, aggiorna il tuo PHP.

Fonti e ulteriori letture:


1
Per favore, spiega quali sono i vantaggi di yeilduna soluzione come questa: ideone.com/xgqevM
Mike

1
Ah, bene, e le comunicazioni che stavo generando. Huh. Bene, ho sperimentato nell'emulazione di generatori per PHP> = 5.0.0 con una classe helper, e sì, leggermente meno leggibile, ma potrei usarlo in futuro. Argomento interessante. Grazie!
Mike,

Non leggibilità ma utilizzo della memoria! Confronta memoria utilizzata per iterare su return range(1,100000000)e for ($i=0; $i<100000000; $i++) yield $i
EMIX

@mike sì, questo è già spiegato nella mia risposta. Nell'altro esempio, la memoria di Mike non è certo un problema perché sta solo ripetendo 10 valori.
Gordon,

1
@Mike Un problema con l'xrange è che il suo uso dei limiti statici è l'utilità per l'annidamento, ad esempio (per esempio la ricerca su una varietà n dimensionale, o un quicksort ricorsivo usando generatori, per esempio). Non è possibile nidificare i loop xrange perché esiste solo una singola istanza del relativo contatore. La versione di Yield non presenta questo problema.
Shayne,

43

Questa funzione utilizza la resa:

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

è quasi uguale a questo senza:

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

L'unica differenza è che a()restituisce un generatore e b()solo un semplice array. Puoi iterare su entrambi.

Inoltre, il primo non alloca un array completo ed è quindi meno impegnativo per la memoria.


2
note addt dai documenti ufficiali: In PHP 5, un generatore non potrebbe restituire un valore: farlo comporterebbe un errore di compilazione. Un'istruzione return vuota era una sintassi valida all'interno di un generatore e avrebbe terminato il generatore. A partire da PHP 7.0, un generatore può restituire valori, che possono essere recuperati usando Generator :: getReturn (). php.net/manual/en/language.generators.syntax.php
Programmatore Dancuk

Semplice e conciso
John Miller,

24

semplice esempio

<?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#';
?>

produzione

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

esempio avanzato

<?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#';
?>

produzione

#start main# {start[0=>1,1=>2,2=>3,3=>4,4=>5,#end main#

Quindi, ritorna senza interrompere la funzione?
Lucas Bustamante,

22

yieldla parola chiave serve per la definizione di "generatori" in PHP 5.5. Ok, allora cos'è un generatore ?

Da php.net:

I generatori forniscono un modo semplice per implementare semplici iteratori senza il sovraccarico o la complessità dell'implementazione di una classe che implementa l'interfaccia Iterator.

Un generatore consente di scrivere codice che utilizza foreach per scorrere su un set di dati senza la necessità di creare un array in memoria, il che può causare il superamento di un limite di memoria o richiedere una notevole quantità di tempo di elaborazione per la generazione. Invece, puoi scrivere una funzione generatore, che è la stessa di una funzione normale, tranne per il fatto che invece di tornare una volta, un generatore può produrre tutte le volte che è necessario per fornire i valori da ripetere.

Da questo posto: generatori = generatori, altre funzioni (solo una semplice funzione) = funzioni.

Quindi, sono utili quando:

  • devi fare cose semplici (o cose semplici);

    Il generatore è davvero molto più semplice rispetto all'implementazione dell'interfaccia Iterator. d'altra parte è, ovviamente, che i generatori sono meno funzionali. confrontali .

  • è necessario generare GRANDI quantità di dati, risparmiando memoria;

    in realtà per risparmiare memoria possiamo semplicemente generare i dati necessari tramite le funzioni per ogni iterazione di loop e dopo l'iterazione utilizzare la spazzatura. quindi qui i punti principali sono: codice chiaro e probabilmente prestazioni. vedi cosa è meglio per le tue esigenze.

  • è necessario generare una sequenza, che dipende da valori intermedi;

    questo si sta estendendo al pensiero precedente. i generatori possono semplificare le cose rispetto alle funzioni. controlla l' esempio di Fibonacci e prova a fare una sequenza senza generatore. Anche in questo caso i generatori possono lavorare più velocemente, almeno a causa della memorizzazione di valori intermedi nelle variabili locali;

  • devi migliorare le prestazioni.

    possono lavorare più velocemente quindi funzionano in alcuni casi (vedi beneficio precedente);


1
Non ho capito come funzionano i generatori. questa classe implementa l'interfaccia iteratore. da quello che so, le classi di iteratori mi permettono di configurare come voglio iterare su un oggetto. ad esempio ArrayIterator ottiene un array o un oggetto in modo che io possa modificare valori e chiavi mentre lo eseguo. quindi se gli iteratori ottengono l'intero oggetto / array, allora in che modo il generatore non deve costruire l'intero array in memoria ???
user3021621

7

Con yieldte puoi facilmente descrivere i punti di interruzione tra più attività in una singola funzione. Questo è tutto, non c'è niente di speciale.

$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 sono altamente correlati, ma è necessario un punto di interruzione tra loro per fare qualcos'altro:

  • memoria libera tra l'elaborazione delle righe del database
  • eseguire altre attività che forniscono dipendenza per l'attività successiva, ma che non sono correlate dalla comprensione del codice corrente
  • fare chiamate asincrone e attendere i risultati
  • e così via ...

quindi i generatori sono la soluzione migliore, perché non è necessario suddividere il codice in molte chiusure o mescolarlo con altro codice o utilizzare callback, ecc ... Basta usare yieldper aggiungere un punto di interruzione e si può continuare da quello punto di interruzione se sei pronto.

Aggiungi punto di interruzione senza generatori:

$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);
//...

Aggiungi punto di interruzione con generatori

$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: è facile commettere errori con i generatori, quindi scrivi sempre unit test prima di implementarli! note2: Usare i generatori in un ciclo infinito è come scrivere una chiusura che ha una lunghezza infinita ...


4

Nessuna delle risposte sopra mostra un esempio concreto che utilizza matrici di massa popolate da membri non numerici. Ecco un esempio usando un array generato da explode()un file .txt di grandi dimensioni (262 MB nel mio caso d'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();

L'output è stato:

Starting memory usage: 415160
Final memory usage: 270948256

Ora confrontalo con uno script simile, usando la yieldparola chiave:

<?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();

L'output per questo script era:

Starting memory usage: 415152
Final memory usage: 415616

Chiaramente i risparmi nell'uso della memoria sono stati considerevoli (ΔMemoryUsage -----> ~ 270,5 MB nel primo esempio, ~ 450B nel secondo esempio).


3

Un aspetto interessante, che vale la pena discutere qui, sta cedendo per riferimento . Ogni volta che è necessario modificare un parametro in modo che venga riflesso all'esterno della funzione, è necessario passare questo parametro per riferimento. Per applicare questo ai generatori, è sufficiente anteporre una e commerciale &al nome del generatore e alla variabile utilizzata nell'iterazione:

 <?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...

L'esempio sopra mostra come la modifica dei valori iterati all'interno del foreachciclo cambia la $fromvariabile all'interno del generatore. Questo perché $fromviene prodotto per riferimento a causa della e commerciale prima del nome del generatore. Per questo $valuemotivo , la variabile all'interno del foreachloop è un riferimento alla $fromvariabile all'interno della funzione del generatore.


0

Il codice seguente mostra come l'utilizzo di un generatore restituisce un risultato prima del completamento, a differenza del tradizionale approccio non generatore che restituisce un array completo dopo l'iterazione completa. Con il generatore di seguito, i valori vengono restituiti quando pronti, non è necessario attendere che un array sia completamente riempito:

<?php 

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

foreach (sleepiterate(5) as $i) {
    echo $i, PHP_EOL;
}

Quindi, non è possibile utilizzare il rendimento per generare codice HTML in PHP? Non conosco i benefici in un ambiente reale
Giuseppe Lodi Rizzini il

@GiuseppeLodiRizzini cosa te lo fa pensare?
Brad Kent,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.