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



visual csharp (11)

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

https://code.i-harness.com

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.


A prima vista, il rendimento restituito è uno zucchero .NET per restituire un oggetto IEnumerable.

Senza rendimento, tutti gli elementi della raccolta vengono creati contemporaneamente:

class SomeData
{
    public SomeData() { }

    static public IEnumerable<SomeData> CreateSomeDatas()
    {
        return new List<SomeData> {
            new SomeData(), 
            new SomeData(), 
            new SomeData()
        };
    }
}

Stesso codice con rendimento, restituisce oggetto per articolo:

class SomeData
{
    public SomeData() { }

    static public IEnumerable<SomeData> CreateSomeDatas()
    {
        yield return new SomeData();
        yield return new SomeData();
        yield return new SomeData();
    }
}

Il vantaggio dell'utilizzo della resa è che se la funzione che consuma i tuoi dati ha semplicemente bisogno del primo elemento della collezione, il resto degli articoli non verrà creato.

L'operatore di rendimento consente la creazione di articoli così come è richiesto. Questa è una buona ragione per usarla.


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


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 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() .


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.


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");
}

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


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]; 
}

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


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.





yield