Uso corretto del "rendimento"


903

La parola chiave yield è una di queste parole chiave in C # che continua a confondermi e non sono mai stato sicuro di usarlo correttamente.

Dei seguenti due pezzi di codice, qual è il preferito e perché?

Versione 1: utilizzo del rendimento

public static IEnumerable<Product> GetAllProducts()
{
    using (AdventureWorksEntities db = new AdventureWorksEntities())
    {
        var products = from product in db.Product
                       select product;

        foreach (Product product in products)
        {
            yield return product;
        }
    }
}

Versione 2: restituisce l'elenco

public static IEnumerable<Product> GetAllProducts()
{
    using (AdventureWorksEntities db = new AdventureWorksEntities())
    {
        var products = from product in db.Product
                       select product;

        return products.ToList<Product>();
    }
}

38
yieldè legato al IEnumerable<T>suo genere. È in qualche modo una valutazione pigra
Jaider il

ecco una risposta greate alla domanda simile. stackoverflow.com/questions/15381708/…
Sanjeev Rai

1
Ecco un buon esempio di utilizzo: stackoverflow.com/questions/3392612/...
Valge

6
Vedo un buon caso per l'utilizzo yield returnse il codice che scorre attraverso i risultati GetAllProducts()consente all'utente la possibilità di annullare anticipatamente l'elaborazione.
JMD

2
Ho trovato questa discussione davvero utile: programmers.stackexchange.com/a/97350/148944
PiotrWolkowski

Risposte:


806

Tendo a utilizzare il rendimento-rendimento quando calcolo l'articolo successivo nell'elenco (o anche il gruppo successivo di articoli).

Utilizzando la versione 2, è necessario disporre dell'elenco completo prima di tornare. Usando il rendimento-rendimento, devi solo avere l'articolo successivo prima di tornare.

Tra le altre cose, questo aiuta a distribuire il costo computazionale di calcoli complessi su un arco temporale più ampio. Ad esempio, se l'elenco è collegato a una GUI e l'utente non passa mai all'ultima pagina, non si calcolano mai gli elementi finali nell'elenco.

Un altro caso in cui è preferibile il rendimento-rendimento è se IEnumerable rappresenta un set infinito. Considera l'elenco di numeri primi o un elenco infinito di numeri casuali. Non è mai possibile restituire l'intero IEnumerable in una volta, quindi utilizzare yield-return per restituire l'elenco in modo incrementale.

Nel tuo esempio particolare, hai l'elenco completo dei prodotti, quindi utilizzerei la versione 2.


31
Vorrei sottolineare che nel tuo esempio nella domanda 3 si fondono due vantaggi. 1) Distribuisce i costi di calcolo (a volte un vantaggio, a volte no) 2) Può pigramente evitare il calcolo indefinitamente in molti casi d'uso. Non si menziona il potenziale svantaggio che mantiene attorno allo stato intermedio. Se si dispone di quantità significative di stato intermedio (ad esempio un HashSet per l'eliminazione dei duplicati), l'uso del rendimento può gonfiare il footprint di memoria.
Kennet Belenky,

8
Inoltre, se ogni singolo elemento è molto grande, ma è necessario accedervi solo in sequenza, una resa è migliore.
Kennet Belenky,

2
E infine ... c'è una tecnica leggermente traballante ma a volte efficace per usare il rendimento per scrivere codice asincrono in una forma molto serializzata.
Kennet Belenky,

12
Un altro esempio che potrebbe essere interessante è quando si leggono file CSV piuttosto grandi. Vuoi leggere ogni elemento ma vuoi anche estrarre la tua dipendenza. La resa che restituisce un IEnumerable <> ti permetterà di restituire ogni riga ed elaborare ciascuna riga singolarmente. Non è necessario leggere un file da 10 Mb in memoria. Solo una riga alla volta.
Maxime Rouiller

1
Yield returnsembra essere una scorciatoia per scrivere la propria classe di iteratore personalizzata (implementare IEnumerator). Pertanto, i vantaggi citati si applicano anche alle classi di iteratore personalizzate. Ad ogni modo, entrambi i costrutti mantengono uno stato intermedio. Nella sua forma più semplice si tratta di tenere un riferimento all'oggetto corrente.
J. Ouwehand,

641

Popolare un elenco temporaneo è come scaricare l'intero video, mentre l'utilizzo yieldè come lo streaming di quel video.


180
Sono perfettamente consapevole che questa risposta non è una risposta tecnica, ma credo che la somiglianza tra rendimento e streaming video sia un buon esempio per comprendere la parola chiave rendimento. Tutto ciò che è tecnico è già stato detto su questo argomento, quindi ho cercato di spiegare "in altre parole". Esiste una regola della comunità che dice che non puoi spiegare le tue idee in termini non tecnici?
anar khalilov

13
Non sono sicuro di chi ti abbia votato in basso o perché (vorrei che lo avessero commentato), ma penso che in qualche modo lo descriva da una prospettiva non tecnica.
senfo,

22
Afferrare ancora il concetto e questo ha contribuito a metterlo ulteriormente a fuoco, bella analogia.
Tony,

11
Mi piace questa risposta, ma non risponde alla domanda.
ANeves

73

Come esempio concettuale per capire quando dovresti usare yield , diciamo che il metodo ConsumeLoop()elabora gli articoli restituiti / prodotti da ProduceList():

void ConsumeLoop() {
    foreach (Consumable item in ProduceList())        // might have to wait here
        item.Consume();
}

IEnumerable<Consumable> ProduceList() {
    while (KeepProducing())
        yield return ProduceExpensiveConsumable();    // expensive
}

Senza yield, la chiamata a ProduceList()potrebbe richiedere molto tempo perché è necessario completare l'elenco prima di tornare:

//pseudo-assembly
Produce consumable[0]                   // expensive operation, e.g. disk I/O
Produce consumable[1]                   // waiting...
Produce consumable[2]                   // waiting...
Produce consumable[3]                   // completed the consumable list
Consume consumable[0]                   // start consuming
Consume consumable[1]
Consume consumable[2]
Consume consumable[3]

Usando yield, diventa riorganizzato, sorta di lavoro "in parallelo":

//pseudo-assembly
Produce consumable[0]
Consume consumable[0]                   // immediately Consume
Produce consumable[1]
Consume consumable[1]                   // consume next
Produce consumable[2]
Consume consumable[2]                   // consume next
Produce consumable[3]
Consume consumable[3]                   // consume next

E infine, come molti hanno già suggerito, dovresti usare la versione 2 perché hai già l'elenco completo.


30

So che questa è una vecchia domanda, ma vorrei offrire un esempio di come la parola chiave yield può essere utilizzata in modo creativo. Ho davvero beneficiato di questa tecnica. Spero che questo possa essere d'aiuto a chiunque altro inciampi su questa domanda.

Nota: non pensare alla parola chiave yield semplicemente come un altro modo per creare una raccolta. Una grande parte del potere del rendimento deriva dal fatto che l'esecuzione è messa in pausa nel metodo o nella proprietà fino a quando il codice chiamante non scorre sul valore successivo. Ecco il mio esempio:

L'uso della parola chiave yield (insieme all'implementazione delle coroutine Caliburn.Micro di Rob Eisenburg ) mi permette di esprimere una chiamata asincrona a un servizio web come questo:

public IEnumerable<IResult> HandleButtonClick() {
    yield return Show.Busy();

    var loginCall = new LoginResult(wsClient, Username, Password);
    yield return loginCall;
    this.IsLoggedIn = loginCall.Success;

    yield return Show.NotBusy();
}

Quello che farà è accendere il mio BusyIndicator, chiamare il metodo Login sul mio servizio web, impostare il mio flag IsLoggedIn sul valore di ritorno, quindi disattivare nuovamente BusyIndicator.

Ecco come funziona: IResult ha un metodo Execute e un evento Completato. Caliburn.Micro prende l'IEnumerator dalla chiamata a HandleButtonClick () e lo passa in un metodo Coroutine.BeginExecute. Il metodo BeginExecute avvia l'iterazione tramite IResults. Quando viene restituito il primo IResult, l'esecuzione viene messa in pausa all'interno di HandleButtonClick () e BeginExecute () collega un gestore eventi all'evento Completato e chiama Execute (). IResult.Execute () può eseguire un'attività sincrona o asincrona e, al termine, genera l'evento Completato.

LoginResult è simile al seguente:

public LoginResult : IResult {
    // Constructor to set private members...

    public void Execute(ActionExecutionContext context) {
        wsClient.LoginCompleted += (sender, e) => {
            this.Success = e.Result;
            Completed(this, new ResultCompletionEventArgs());
        };
        wsClient.Login(username, password);
    }

    public event EventHandler<ResultCompletionEventArgs> Completed = delegate { };
    public bool Success { get; private set; }
}

Può essere utile impostare qualcosa del genere e seguire l'esecuzione per vedere cosa sta succedendo.

Spero che questo aiuti qualcuno! Mi è piaciuto molto esplorare i diversi modi in cui è possibile utilizzare la resa.


1
l'esempio di codice è un eccellente esempio di come utilizzare il rendimento ESTERNO di un blocco for o foreach. La maggior parte degli esempi mostra il rendimento in un iteratore. Molto utile in quanto stavo per porre la domanda su SO Come utilizzare la resa al di fuori di un iteratore!
shelbypereira,

Non mi è mai venuto in mente di usarlo yieldin questo modo. Sembra un modo elegante per emulare il modello asincrono / wait (che presumo sarebbe usato al posto di yieldse questo fosse riscritto oggi). Hai scoperto che questi usi creativi yieldhanno prodotto (senza intendere giochi di parole) rendimenti decrescenti nel corso degli anni poiché C # si è evoluto da quando hai risposto a questa domanda? Oppure stai ancora pensando a casi d'uso intelligenti e modernizzati come questo? E se è così, ti dispiacerebbe condividere un altro scenario interessante per noi?
Sbilenco il

27

Sembrerà un bizzarro suggerimento, ma ho imparato a usare la yieldparola chiave in C # leggendo una presentazione sui generatori in Python: http://www.dabeaz.com/generators/Generators.pdf di David M. Beazley . Non è necessario conoscere molto Python per capire la presentazione - non l'ho fatto. L'ho trovato molto utile per spiegare non solo come funzionano i generatori, ma perché dovresti preoccupartene.


1
La presentazione offre una semplice panoramica. I dettagli di come funziona in C # sono discussi da Ray Chen nei collegamenti su stackoverflow.com/a/39507/939250 Il primo collegamento spiega in dettaglio che esiste un secondo ritorno implicito alla fine dei metodi di rendimento.
Donal Lafferty,

18

Il rendimento può essere molto potente per gli algoritmi in cui è necessario scorrere tra milioni di oggetti. Considera l'esempio seguente in cui è necessario calcolare i possibili viaggi per la condivisione delle corse. Innanzitutto generiamo possibili viaggi:

    static IEnumerable<Trip> CreatePossibleTrips()
    {
        for (int i = 0; i < 1000000; i++)
        {
            yield return new Trip
            {
                Id = i.ToString(),
                Driver = new Driver { Id = i.ToString() }
            };
        }
    }

Quindi scorrere ogni viaggio:

    static void Main(string[] args)
    {
        foreach (var trip in CreatePossibleTrips())
        {
            // possible trip is actually calculated only at this point, because of yield
            if (IsTripGood(trip))
            {
                // match good trip
            }
        }
    }

Se si utilizza Elenco invece di rendimento, sarà necessario allocare 1 milione di oggetti alla memoria (~ 190mb) e questo semplice esempio richiederà ~ 1400ms per l'esecuzione. Tuttavia, se usi la resa, non hai bisogno di mettere tutti questi oggetti temporanei in memoria e otterrai una velocità dell'algoritmo significativamente più veloce: questo esempio richiederà solo ~ 400ms per funzionare senza consumo di memoria.


2
sotto le coperte che cos'è la resa? Avrei pensato che fosse un elenco, quindi come avrebbe migliorato l'utilizzo della memoria?
lancia

1
@rolls yieldfunziona sotto copertura implementando internamente una macchina a stati. Ecco una risposta SO con 3 post dettagliati sul blog MSDN che spiegano l'implementazione in modo dettagliato. Scritto da Raymond Chen @ MSFT
Shiva il

13

I due pezzi di codice stanno davvero facendo due cose diverse. La prima versione attirerà i membri di cui hai bisogno. La seconda versione caricherà tutti i risultati in memoria prima di iniziare a fare qualsiasi cosa con esso.

Non esiste una risposta giusta o sbagliata a questo. Quale è preferibile dipende solo dalla situazione. Ad esempio, se c'è un limite di tempo per completare la query e devi fare qualcosa di semi complicato con i risultati, la seconda versione potrebbe essere preferibile. Ma fai attenzione ai set di risultati di grandi dimensioni, soprattutto se stai eseguendo questo codice in modalità a 32 bit. Sono stato morso da eccezioni OutOfMemory diverse volte durante questo metodo.

La cosa chiave da tenere a mente è questa però: le differenze sono nell'efficienza. Pertanto, dovresti probabilmente scegliere quello che semplifica il tuo codice e modificarlo solo dopo aver creato il profilo.


11

La resa ha due grandi usi

Aiuta a fornire iterazioni personalizzate senza la creazione di raccolte temporanee. (caricamento di tutti i dati e loop)

Aiuta a eseguire iterazioni con stato. ( streaming)

Di seguito è riportato un semplice video che ho creato con dimostrazione completa al fine di supportare i due punti precedenti

http://www.youtube.com/watch?v=4fju3xcm21M


10

Questo è ciò che Chris Sells racconta di quelle affermazioni in The C # Programming Language ;

A volte dimentico che il rendimento non è uguale al rendimento, in quanto il codice dopo un rendimento può essere eseguito. Ad esempio, il codice dopo il primo ritorno qui non può mai essere eseguito:

    int F() {
return 1;
return 2; // Can never be executed
}

Al contrario, il codice dopo il primo rendimento restituito qui può essere eseguito:

IEnumerable<int> F() {
yield return 1;
yield return 2; // Can be executed
}

Questo mi morde spesso in un'istruzione if:

IEnumerable<int> F() {
if(...) { yield return 1; } // I mean this to be the only
// thing returned
yield return 2; // Oops!
}

In questi casi, è utile ricordare che il rendimento non è "finale".


per ridurre l'ambiguità, ti preghiamo di chiarire quando dici che può, è, sarà o potrebbe? potrebbe essere possibile che il primo ritorni e non esegua il secondo rendimento?
Johno Crawford,

@JohnoCrawford la seconda dichiarazione di rendimento verrà eseguita solo se viene elencato il secondo / prossimo valore di IEnumerable. È del tutto possibile che non lo faccia, ad es F().Any(). Questo ritornerà dopo aver tentato di elencare solo il primo risultato. In generale, non dovresti fare affidamento su un IEnumerable yieldper cambiare lo stato del programma, perché potrebbe non essere effettivamente attivato
Zac Faragher,

8

Supponendo che i tuoi prodotti della classe LINQ utilizzino un rendimento simile per l'enumerazione / iterazione, la prima versione è più efficiente perché fornisce solo un valore ogni volta che viene ripetuta.

Il secondo esempio sta convertendo l'enumeratore / iteratore in un elenco con il metodo ToList (). Ciò significa che scorre manualmente su tutti gli elementi nell'enumeratore e quindi restituisce un elenco semplice.


8

Questo è un po 'oltre il punto, ma dal momento che la domanda è taggata best practice andrò avanti e aggiungerò i miei due centesimi. Per questo tipo di cose preferisco di gran lunga trasformarlo in una proprietà:

public static IEnumerable<Product> AllProducts
{
    get {
        using (AdventureWorksEntities db = new AdventureWorksEntities()) {
            var products = from product in db.Product
                           select product;

            return products;
        }
    }
}

Certo, è un po 'più piatto di caldaia, ma il codice che lo utilizza sembrerà molto più pulito:

prices = Whatever.AllProducts.Select (product => product.price);

vs

prices = Whatever.GetAllProducts().Select (product => product.price);

Nota: non lo farei per alcun metodo che potrebbe richiedere del tempo per svolgere il proprio lavoro.


7

E che dire di questo?

public static IEnumerable<Product> GetAllProducts()
{
    using (AdventureWorksEntities db = new AdventureWorksEntities())
    {
        var products = from product in db.Product
                       select product;

        return products.ToList();
    }
}

Immagino sia molto più pulito. Tuttavia, non ho VS2008 a portata di mano. In ogni caso, se Products implementa IEnumerable (come sembra - viene utilizzato in un'istruzione foreach), lo restituirei direttamente.


2
Modifica OP per includere più informazioni invece di inviare risposte.
Brian Rasmussen,

Bene, dovresti dirmi cosa significa esattamente OP :-) Grazie
petr k.

Posta originale, presumo. Non riesco a modificare i post, quindi questa sembra essere la strada da percorrere.
petr k.

5

In questo caso avrei usato la versione 2 del codice. Dal momento che hai l'elenco completo dei prodotti disponibili ed è quello che il "consumatore" si aspetta da questa chiamata del metodo, sarebbe necessario rispedire al chiamante le informazioni complete.

Se il chiamante di questo metodo richiede "una" informazione alla volta e il consumo delle informazioni successive è su richiesta, sarebbe utile utilizzare il rendimento restituito che assicurerà che il comando di esecuzione venga restituito al chiamante quando è disponibile un'unità di informazioni.

Alcuni esempi in cui si potrebbe usare il rendimento sono:

  1. Calcolo complesso e dettagliato in cui il chiamante è in attesa dei dati di un passaggio alla volta
  2. Paging nella GUI - dove l'utente potrebbe non raggiungere mai l'ultima pagina e solo il sottoinsieme di informazioni è richiesto per essere divulgato nella pagina corrente

Per rispondere alle tue domande, avrei usato la versione 2.


3

Restituisce l'elenco direttamente. Benefici:

  • È più chiaro
  • L'elenco è riutilizzabile. (l'iteratore non lo è) in realtà non è vero, grazie Jon

Dovresti usare l'iteratore (rendimento) da quando pensi che probabilmente non dovrai iterare fino alla fine dell'elenco o quando non ha fine. Ad esempio, la chiamata del client cercherà il primo prodotto che soddisfa un predicato, potresti considerare di utilizzare l'iteratore, anche se questo è un esempio inventato, e probabilmente ci sono modi migliori per realizzarlo. Fondamentalmente, se sai in anticipo che sarà necessario calcolare l'intero elenco, fallo in anticipo. Se pensi che non lo farà, prendi in considerazione l'utilizzo della versione iteratore.


Non dimenticare che sta tornando in IEnumerable <T>, non in IEnumerator <T>: puoi chiamare nuovamente GetEnumerator.
Jon Skeet,

Anche se si conosce in anticipo l'intero elenco dovrà essere calcolato, potrebbe essere comunque utile utilizzare il rendimento. Un esempio è quando la raccolta contiene centinaia di migliaia di articoli.
Val

1

La chiave di ritorno del rendimento viene utilizzata per mantenere la macchina a stati per una particolare raccolta. Ovunque CLR vede la frase chiave di ritorno del rendimento utilizzata, CLR implementa un modello Enumerator in quel pezzo di codice. Questo tipo di implementazione aiuta lo sviluppatore da tutto il tipo di impianto idraulico che altrimenti avremmo dovuto fare in assenza della parola chiave.

Supponiamo che lo sviluppatore stia filtrando una raccolta, ripetendo la raccolta e quindi estraendo tali oggetti in una nuova raccolta. Questo tipo di impianto idraulico è abbastanza monotono.

Ulteriori informazioni sulla parola chiave qui in questo articolo .


-4

L'uso del rendimento è simile alla parola chiave return , tranne per il fatto che restituirà un generatore . E l' oggetto generatore attraverserà solo una volta .

la resa ha due vantaggi:

  1. Non è necessario leggere questi valori due volte;
  2. È possibile ottenere molti nodi figlio ma non è necessario metterli tutti in memoria.

C'è un'altra chiara spiegazione che potrebbe aiutarti.

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.