variable - Qual è la parola chiave yield utilizzata in C#?




modifiers modificatori (14)

Nella domanda Come posso esporre solo un frammento di IList <> una delle risposte aveva il seguente frammento di codice:

IEnumerable<object> FilteredList()
{
    foreach( object item in FullList )
    {
        if( IsItemInPartialList( item )
            yield return item;
    }
}

Cosa fa la parola chiave yield qui? L'ho visto referenziato in un paio di posti, e un'altra domanda, ma non ho ancora capito cosa effettivamente fa. Sono abituato a pensare alla resa nel senso che un thread cede a un altro, ma non sembra rilevante qui.


Answers

Sta cercando di portare un po 'di Ruby Goodness :)
Concetto: questo è un esempio di codice rubino che stampa ogni elemento dell'array

 rubyArray = [1,2,3,4,5,6,7,8,9,10]
    rubyArray.each{|x| 
        puts x   # do whatever with x
    }

L'implementazione del metodo di ciascuna matrice fornisce il controllo sul chiamante (il 'puts x') con ogni elemento dell'array presentato in modo ordinato come x. Il chiamante può quindi fare tutto ciò che deve fare con x.

Tuttavia .Net non va fino in fondo qui .. C # sembra avere un rendimento accoppiato con IEnumerable, in un modo che ti costringe a scrivere un ciclo foreach nel chiamante come si vede nella risposta di Mendelt. Poco meno elegante.

//calling code
foreach(int i in obCustomClass.Each())
{
    Console.WriteLine(i.ToString());
}

// CustomClass implementation
private int[] data = {1,2,3,4,5,6,7,8,9,10};
public IEnumerable<int> Each()
{
   for(int iLooper=0; iLooper<data.Length; ++iLooper)
        yield return data[iLooper]; 
}

Intuitivamente, la parola chiave restituisce un valore dalla funzione senza lasciarlo, cioè nell'esempio di codice restituisce il valore corrente item e quindi riprende il ciclo. Più formalmente, è usato dal compilatore per generare codice per un iteratore . Iterator sono funzioni che restituiscono oggetti IEnumerable . Il MSDN ha diversi articles su di loro.


La parola chiave yield C #, per dirla in parole semplici, consente molte chiamate a un corpo di codice, indicato come iteratore, che sa come tornare prima che sia fatto e, quando chiamato di nuovo, continua da dove era stato interrotto - cioè aiuta un iteratore diventa trasparente per ogni oggetto in una sequenza che l'iteratore ritorna nelle chiamate successive.

In JavaScript, lo stesso concetto si chiama Generatori.


Se ho capito bene, ecco come esprimerei questo dal punto di vista della funzione che implementa IEnumerable con yield.

  • Eccone uno
  • Chiama di nuovo se ne hai bisogno di un altro.
  • Ricorderò quello che ti ho già dato.
  • Saprò solo se posso darti un altro quando chiami di nuovo.

La parola chiave yield consente di creare un oggetto IEnumerable<T> nel modulo su un blocco iteratore . Questo blocco iteratore supporta l' esecuzione differita e, se non si ha familiarità con il concetto, potrebbe sembrare quasi magico. Tuttavia, alla fine della giornata è solo il codice che esegue senza trucchi strani.

Un blocco iteratore può essere descritto come zucchero sintattico in cui il compilatore genera una macchina a stati che tiene traccia di quanto è progredita l'enumerazione dell'enumerabile. Per enumerare una enumerabile, si usa spesso un ciclo foreach . Tuttavia, un ciclo di foreach è anche zucchero sintattico. Quindi tu sei due astrazioni rimosse dal codice reale ed è per questo che inizialmente potrebbe essere difficile capire come tutto funzioni insieme.

Supponiamo che tu abbia un blocco iteratore molto semplice:

IEnumerable<int> IteratorBlock()
{
    Console.WriteLine("Begin");
    yield return 1;
    Console.WriteLine("After 1");
    yield return 2;
    Console.WriteLine("After 2");
    yield return 42;
    Console.WriteLine("End");
}

I veri blocchi iteratori hanno spesso condizioni e cicli, ma quando si controllano le condizioni e si srotolano i loop continuano a essere dichiarazioni di yield intercalate con altri codici.

Per enumerare il blocco iteratore viene utilizzato un ciclo foreach :

foreach (var i in IteratorBlock())
    Console.WriteLine(i);

Ecco l'output (senza sorprese qui):

Begin
1
After 1
2
After 2
42
End

Come detto sopra, foreach è zucchero sintattico:

IEnumerator<int> enumerator = null;
try
{
    enumerator = IteratorBlock().GetEnumerator();
    while (enumerator.MoveNext())
    {
        var i = enumerator.Current;
        Console.WriteLine(i);
    }
}
finally
{
    enumerator?.Dispose();
}

Nel tentativo di districare ciò ho creato un diagramma di sequenza con le astrazioni rimosse:

La macchina a stati generata dal compilatore implementa anche l'enumeratore ma per rendere il diagramma più chiaro li ho mostrati come istanze separate. (Quando la macchina a stati viene enumerata da un altro thread, si ottengono in realtà istanze separate, ma quel dettaglio non è importante qui.)

Ogni volta che si chiama il blocco iteratore viene creata una nuova istanza della macchina a stati. Tuttavia, nessuno del codice nel blocco iteratore viene eseguito finché enumerator.MoveNext() viene eseguito per la prima volta. Questo è il modo in cui l'esecuzione differita funziona. Ecco un esempio (piuttosto stupido):

var evenNumbers = IteratorBlock().Where(i => i%2 == 0);

A questo punto l'iteratore non è stato eseguito. La clausola Where crea un nuovo IEnumerable<T> che racchiude l' IEnumerable<T> restituito da IteratorBlock ma questa enumerable deve ancora essere enumerata. Questo succede quando si esegue un ciclo foreach :

foreach (var evenNumber in evenNumbers)
    Console.WriteLine(eventNumber);

Se si enumera l'enumerabile due volte, viene creata una nuova istanza della macchina a stati ogni volta e il blocco iteratore eseguirà lo stesso codice due volte.

Si noti che i metodi LINQ come ToList() , ToArray() , First() , Count() ecc. Utilizzeranno un ciclo foreach per enumerare l'enumerabile. Ad esempio ToList() enumera tutti gli elementi di enumerable e li memorizza in un elenco. È ora possibile accedere all'elenco per ottenere tutti gli elementi di enumerable senza che il blocco iteratore esegua nuovamente. Esiste un compromesso tra l'utilizzo della CPU per produrre gli elementi di enumerazione più volte e la memoria per memorizzare gli elementi dell'enumerazione per accedervi più volte quando si utilizzano metodi come ToList() .


Un'implementazione di lista o array carica immediatamente tutti gli articoli mentre l'implementazione del rendimento fornisce una soluzione di esecuzione differita.

In pratica, è spesso auspicabile eseguire la quantità minima di lavoro necessaria per ridurre il consumo di risorse di un'applicazione.

Ad esempio, potremmo avere un'applicazione che elabora milioni di record da un database. I seguenti vantaggi possono essere ottenuti quando si utilizza IEnumerable in un modello basato su pull in esecuzione differita:

  • Scalabilità, affidabilità e prevedibilità sono in grado di migliorare poiché il numero di record non influisce in modo significativo sui requisiti delle risorse dell'applicazione.
  • È probabile che le prestazioni e la reattività migliorino poiché l'elaborazione può iniziare immediatamente anziché attendere che l'intera raccolta venga caricata per prima.
  • È probabile che il recupero e l'utilizzo migliorino poiché l'applicazione può essere arrestata, avviata, interrotta o fallita. Solo le voci in corso saranno perse rispetto al pre-recupero di tutti i dati in cui è stata effettivamente utilizzata solo una parte dei risultati.
  • L'elaborazione continua è possibile in ambienti in cui vengono aggiunti flussi di carichi di lavoro costanti.

Ecco un confronto tra la compilazione di una raccolta prima come un elenco rispetto all'utilizzo di rendimento.

Esempio di lista

    public class ContactListStore : IStore<ContactModel>
    {
        public IEnumerable<ContactModel> GetEnumerator()
        {
            var contacts = new List<ContactModel>();
            Console.WriteLine("ContactListStore: Creating contact 1");
            contacts.Add(new ContactModel() { FirstName = "Bob", LastName = "Blue" });
            Console.WriteLine("ContactListStore: Creating contact 2");
            contacts.Add(new ContactModel() { FirstName = "Jim", LastName = "Green" });
            Console.WriteLine("ContactListStore: Creating contact 3");
            contacts.Add(new ContactModel() { FirstName = "Susan", LastName = "Orange" });
            return contacts;
        }
    }

    static void Main(string[] args)
    {
        var store = new ContactListStore();
        var contacts = store.GetEnumerator();

        Console.WriteLine("Ready to iterate through the collection.");
        Console.ReadLine();
    }

Uscita console
ContactListStore: creazione di un contatto 1
ContactListStore: creazione del contatto 2
ContactListStore: creazione di un contatto 3
Pronto per scorrere la collezione.

Nota: l'intera raccolta è stata caricata in memoria senza nemmeno chiedere un singolo elemento nell'elenco

Esempio di resa

public class ContactYieldStore : IStore<ContactModel>
{
    public IEnumerable<ContactModel> GetEnumerator()
    {
        Console.WriteLine("ContactYieldStore: Creating contact 1");
        yield return new ContactModel() { FirstName = "Bob", LastName = "Blue" };
        Console.WriteLine("ContactYieldStore: Creating contact 2");
        yield return new ContactModel() { FirstName = "Jim", LastName = "Green" };
        Console.WriteLine("ContactYieldStore: Creating contact 3");
        yield return new ContactModel() { FirstName = "Susan", LastName = "Orange" };
    }
}

static void Main(string[] args)
{
    var store = new ContactYieldStore();
    var contacts = store.GetEnumerator();

    Console.WriteLine("Ready to iterate through the collection.");
    Console.ReadLine();
}

Uscita console
Pronto per scorrere la collezione.

Nota: la raccolta non è stata eseguita affatto. Ciò è dovuto alla natura di "esecuzione differita" di IEnumerable. La costruzione di un oggetto avverrà solo quando è realmente necessario.

Chiamiamo nuovamente la raccolta e inviamo il comportamento contrario quando recuperiamo il primo contatto nella raccolta.

static void Main(string[] args)
{
    var store = new ContactYieldStore();
    var contacts = store.GetEnumerator();
    Console.WriteLine("Ready to iterate through the collection");
    Console.WriteLine("Hello {0}", contacts.First().FirstName);
    Console.ReadLine();
}

Uscita console
Pronto per scorrere la collezione
ContactYieldStore: creazione di un contatto 1
Ciao Bob

Bello! Solo il primo contatto è stato creato quando il cliente ha "estratto" l'oggetto dalla raccolta.


È un modo molto semplice e semplice per creare un'enumerabile per il tuo oggetto. Il compilatore crea una classe che avvolge il tuo metodo e che implementa, in questo caso, IEnumerable <oggetto>. Senza la parola chiave yield, dovresti creare un oggetto che implementa IEnumerable <oggetto>.


yield return è utilizzato con gli enumeratori. Su ogni call of yield statement, il controllo viene restituito al chiamante, ma garantisce che lo stato del callee sia mantenuto. A causa di ciò, quando il chiamante enumera l'elemento successivo, continua l'esecuzione nel metodo callee dall'istruzione immediatamente dopo l'istruzione yield .

Cerchiamo di capirlo con un esempio. In questo esempio, corrispondente a ciascuna riga, ho menzionato l'ordine in cui scorre l'esecuzione.

static void Main(string[] args)
{
    foreach (int fib in Fibs(6))//1, 5
    {
        Console.WriteLine(fib + " ");//4, 10
    }            
}

static IEnumerable<int> Fibs(int fibCount)
{
    for (int i = 0, prevFib = 0, currFib = 1; i < fibCount; i++)//2
    {
        yield return prevFib;//3, 9
        int newFib = prevFib + currFib;//6
        prevFib = currFib;//7
        currFib = newFib;//8
    }
}

Inoltre, lo stato viene mantenuto per ogni enumerazione. Supponiamo, ho un'altra chiamata al metodo Fibs() , quindi lo stato verrà ripristinato per questo.


Il rendimento ha due grandi usi,

  1. Aiuta a fornire iterazioni personalizzate senza creare collezioni temporanee.

  2. Aiuta a fare iterazioni stateful.

Per spiegare sopra due punti in modo più dimostrativo, ho creato un semplice video che puoi vedere here


Sta producendo una sequenza enumerabile. Quello che fa è in realtà la creazione della sequenza IEnumerable locale e il suo ritorno come risultato del metodo


Questo link ha un semplice esempio

Anche esempi più semplici sono qui

public static IEnumerable<int> testYieldb()
{
    for(int i=0;i<3;i++) yield return 4;
}

Si noti che il rendimento restituito non verrà restituito dal metodo. Puoi persino inserire una WriteLine dopo il yield return

Quanto sopra produce un IEnumerable di 4 inte 4,4,4,4

Qui con una WriteLine . Aggiungerà 4 alla lista, stamperà abc, quindi aggiungerà 4 alla lista, quindi completerà il metodo e quindi tornerà davvero dal metodo (una volta che il metodo è stato completato, come accadrebbe con una procedura senza un ritorno). Ma questo avrebbe un valore, un elenco IEnumerable di int s, che ritorna al completamento.

public static IEnumerable<int> testYieldb()
{
    yield return 4;
    console.WriteLine("abc");
    yield return 4;
}

Si noti inoltre che quando si utilizza la resa, ciò che si sta restituendo non è dello stesso tipo della funzione. È del tipo di elemento all'interno dell'elenco IEnumerable .

Si utilizza la resa con il tipo restituito del metodo come IEnumerable . Se il metodo restituito dal metodo è int o List<int> e si utilizza yield , non verrà compilato. È possibile utilizzare il metodo restituito dal metodo IEnumerable senza rendimento ma sembra che non sia possibile utilizzare il rendimento senza il tipo restituito dal metodo IEnumerable .

E per farlo eseguire devi chiamarlo in un modo speciale.

static void Main(string[] args)
{
    testA();
    Console.Write("try again. the above won't execute any of the function!\n");

    foreach (var x in testA()) { }


    Console.ReadLine();
}



// static List<int> testA()
static IEnumerable<int> testA()
{
    Console.WriteLine("asdfa");
    yield return 1;
    Console.WriteLine("asdf");
}

Ecco un modo semplice per comprendere il concetto: l'idea di base è, se si desidera una raccolta che è possibile utilizzare " foreach ", ma la raccolta degli elementi nella raccolta è costosa per qualche motivo (come l'interrogazione di un database) , E spesso non hai bisogno dell'intera collezione, quindi crei una funzione che costruisce la collezione un elemento alla volta e la restituisce al consumatore (che può quindi interrompere in anticipo lo sforzo di raccolta).

Pensaci in questo modo: vai al bancone della carne e vuoi comprare un chilo di prosciutto affettato. Il macellaio prende un prosciutto da 10 libbre sul retro, lo mette sull'affettatrice, taglia l'intera cosa, poi ti riporta la pila di fette e ne misura un chilo. (VECCHIO modo). Con la yield , il macellaio porta la macchina affettatrice sul bancone e inizia a tagliare ea "cedere" ogni fetta sulla scala finché non misura 1 libbra, quindi la avvolge per te e il gioco è fatto. The Old Way può essere migliore per il macellaio (gli consente di organizzare i suoi macchinari come piace a lui), ma il New Way è chiaramente più efficiente nella maggior parte dei casi per il consumatore.


Recentemente Raymond Chen ha anche pubblicato un'interessante serie di articoli sulla parola chiave yield.

Mentre è nominalmente utilizzato per implementare facilmente un pattern iteratore, ma può essere generalizzato in una macchina a stati. Inutile citare Raymond, l'ultima parte si collega anche ad altri usi (ma l'esempio nel blog di Entin è molto buono, mostrando come scrivere un codice sicuro asincrono).


Ho scritto un'estensione per scorrere su un dizionario.

public static class DictionaryExtension
{
    public static void ForEach<T1, T2>(this Dictionary<T1, T2> dictionary, Action<T1, T2> action) {
        foreach(KeyValuePair<T1, T2> keyValue in dictionary) {
            action(keyValue.Key, keyValue.Value);
        }
    }
}

Quindi puoi chiamare

myDictionary.ForEach((x,y) => Console.WriteLine(x + " - " + y));




c# yield