Ricerca…


introduzione

Quando si utilizza la parola chiave yield in un'istruzione, si indica che il metodo, l'operatore o l'opzione di accesso in cui appare è un iteratore. L'utilizzo di yield per definire un iteratore rimuove la necessità di una classe extra esplicita (la classe che contiene lo stato per un'enumerazione) quando si implementa il modello IEnumerable e IEnumerator per un tipo di raccolta personalizzato.

Sintassi

  • yield return [TYPE]
  • cedimento

Osservazioni

Inserendo la parola chiave yield in un metodo con il tipo restituito di IEnumerable , IEnumerable<T> , IEnumerator o IEnumerator<T> indica al compilatore di generare un'implementazione del tipo restituito ( IEnumerable o IEnumerator ) che, una volta eseguito il loop, esegue il metodo fino a ciascun "rendimento" per ottenere ogni risultato.

La parola chiave yield è utile quando si desidera restituire "il prossimo" elemento di una sequenza teoricamente illimitata, quindi calcolare l'intera sequenza in anticipo sarebbe impossibile, o quando calcolare la sequenza completa di valori prima di tornare comporterebbe una pausa indesiderata per l'utente .

yield break può essere utilizzata anche per interrompere la sequenza in qualsiasi momento.

Poiché la parola chiave yield richiede un tipo di interfaccia iteratore come tipo restituito, ad esempio IEnumerable<T> , non è possibile utilizzarlo in un metodo async poiché restituisce un oggetto Task<IEnumerable<T>> .

Ulteriori letture

Uso semplice

La parola chiave yield viene utilizzata per definire una funzione che restituisce un oggetto IEnumerable o IEnumerator (nonché le varianti generiche derivate) i cui valori vengono generati pigramente mentre un chiamante esegue iterazioni sulla raccolta restituita. Maggiori informazioni sullo scopo nella sezione commenti .

L'esempio seguente ha un'istruzione return return all'interno di un ciclo for .

public static IEnumerable<int> Count(int start, int count)
{
    for (int i = 0; i <= count; i++)
    {
        yield return start + i;
    }
}

Quindi puoi chiamarlo:

foreach (int value in Count(start: 4, count: 10))
{
    Console.WriteLine(value);
}

Uscita console

4
5
6
...
14

Live Demo su .NET Fiddle

Ogni iterazione del corpo foreach crea una chiamata alla funzione Count iteratore. Ogni chiamata alla funzione iteratore procede alla successiva esecuzione yield return , che si verifica durante la successiva iterazione del ciclo for .

Più l'uso pertinente

public IEnumerable<User> SelectUsers()
{
    // Execute an SQL query on a database.
    using (IDataReader reader = this.Database.ExecuteReader(CommandType.Text, "SELECT Id, Name FROM Users"))
    {
        while (reader.Read())
        {
            int id = reader.GetInt32(0);
            string name = reader.GetString(1);
            yield return new User(id, name);
        }
    }
}

Ci sono altri modi per ottenere un IEnumerable<User> da un database SQL, questo dimostra semplicemente che puoi usare yield per trasformare qualsiasi cosa abbia una semantica di "sequence of elements" in un IEnumerable<T> che qualcuno può iterare su .

Risoluzione anticipata

È possibile estendere la funzionalità dei metodi di yield esistenti passando uno o più valori o elementi che potrebbero definire una condizione di terminazione all'interno della funzione chiamando un'interruzione di yield break per interrompere l'esecuzione del ciclo interno.

public static IEnumerable<int> CountUntilAny(int start, HashSet<int> earlyTerminationSet)
{
    int curr = start;

    while (true)
    {
        if (earlyTerminationSet.Contains(curr))
        {
            // we've hit one of the ending values
            yield break;
        }

        yield return curr;

        if (curr == Int32.MaxValue)
        {
            // don't overflow if we get all the way to the end; just stop
            yield break;
        }

        curr++;
    }
}

Il metodo sopra riportato itererà da una data posizione di start fino a earlyTerminationSet si incontra uno dei valori all'interno del primo earlyTerminationSet .

// Iterate from a starting point until you encounter any elements defined as 
// terminating elements
var terminatingElements = new HashSet<int>{ 7, 9, 11 };
// This will iterate from 1 until one of the terminating elements is encountered (7)
foreach(var x in CountUntilAny(1,terminatingElements))
{
    // This will write out the results from 1 until 7 (which will trigger terminating)
    Console.WriteLine(x);
}

Produzione:

1
2
3
4
5
6

Live Demo su .NET Fiddle

Controllo corretto degli argomenti

Un metodo iteratore non viene eseguito fino a quando il valore di ritorno non viene enumerato. È quindi vantaggioso affermare le precondizioni al di fuori dell'iteratore.

public static IEnumerable<int> Count(int start, int count)
{
    // The exception will throw when the method is called, not when the result is iterated
    if (count < 0)
        throw new ArgumentOutOfRangeException(nameof(count));

    return CountCore(start, count);
}

private static IEnumerable<int> CountCore(int start, int count)
{
    // If the exception was thrown here it would be raised during the first MoveNext()
    // call on the IEnumerator, potentially at a point in the code far away from where
    // an incorrect value was passed.
    for (int i = 0; i < count; i++)
    {
        yield return start + i;
    }
}

Calling Side Code (Uso):

// Get the count
var count = Count(1,10);
// Iterate the results
foreach(var x in count)
{
    Console.WriteLine(x);
}

Produzione:

1
2
3
4
5
6
7
8
9
10

Live Demo su .NET Fiddle

Quando un metodo usa yield per generare un enumerabile, il compilatore crea una macchina a stati che, una volta iterata, eseguirà il codice fino a un yield . Quindi restituisce l'articolo restituito e salva il suo stato.

Ciò significa che non si scopriranno argomenti non validi (passaggio null ecc.) Quando si chiama il metodo per la prima volta (poiché ciò crea la macchina di stato), solo quando si tenta di accedere al primo elemento (perché solo allora il codice all'interno del il metodo viene eseguito dalla macchina di stato). Racchiudendolo in un metodo normale che prima controlla gli argomenti è possibile controllarli quando viene chiamato il metodo. Questo è un esempio di veloce fallimento.

Quando si utilizza C # 7+, la funzione CountCore può essere opportunamente nascosta nella funzione Count come funzione locale . Vedi l'esempio qui .

Restituisce un altro Enumerable all'interno di un metodo che restituisce Enumerable

public IEnumerable<int> F1()
{
    for (int i = 0; i < 3; i++)
        yield return i;

    //return F2(); // Compile Error!!
    foreach (var element in F2())
        yield return element;
}

public int[] F2()
{
    return new[] { 3, 4, 5 };
}

Valutazione pigra

Solo quando l'istruzione foreach sposta sull'elemento successivo, il blocco iteratore valuta fino alla successiva dichiarazione di yield .

Considera il seguente esempio:

private IEnumerable<int> Integers()
{
    var i = 0;
    while(true)
    {
        Console.WriteLine("Inside iterator: " + i);
        yield return i;
        i++;
    }
}

private void PrintNumbers()
{
    var numbers = Integers().Take(3);
    Console.WriteLine("Starting iteration");

    foreach(var number in numbers)
    {
        Console.WriteLine("Inside foreach: " + number);
    }
}

Questo produrrà:

Inizia l'iterazione
All'interno dell'iteratore: 0
Dentro foreach: 0
All'interno di iteratore: 1
Dentro foreach: 1
All'interno di iteratore: 2
Dentro foreach: 2

Visualizza la demo

Come conseguenza:

  • "Inizializzazione iterazione" viene stampata prima anche se il metodo iteratore è stato chiamato prima della riga che lo stampa perché la riga Integers().Take(3); non avvia effettivamente l'iterazione (non è stata effettuata alcuna chiamata a IEnumerator.MoveNext() )
  • Le linee che stampano su console si alternano tra quella all'interno del metodo iteratore e quella all'interno del foreach , piuttosto che tutte quelle che si trovano nel metodo iteratore valutando prima
  • Questo programma termina a causa del metodo .Take() , anche se il metodo iteratore ha un while true che non si interrompe mai.

Prova ... finalmente

Se un metodo iteratore ha un rendimento all'interno di una try...finally , l' IEnumerator restituito eseguirà l'istruzione finally quando viene chiamato Dispose , a condizione che il punto di valutazione corrente si trovi all'interno del blocco try .

Data la funzione:

private IEnumerable<int> Numbers()
{
    yield return 1;
    try
    {
        yield return 2;
        yield return 3;
    }
    finally
    {
        Console.WriteLine("Finally executed");
    }
}

Quando si chiama:

private void DisposeOutsideTry()
{
    var enumerator = Numbers().GetEnumerator();

    enumerator.MoveNext();
    Console.WriteLine(enumerator.Current);
    enumerator.Dispose();
}

Quindi stampa:

1

Visualizza la demo

Quando si chiama:

private void DisposeInsideTry()
{
    var enumerator = Numbers().GetEnumerator();

    enumerator.MoveNext();
    Console.WriteLine(enumerator.Current);
    enumerator.MoveNext();
    Console.WriteLine(enumerator.Current);
    enumerator.Dispose();
}

Quindi stampa:

1
2
Finalmente giustiziato

Visualizza la demo

Usare yield per creare un IEnumerator quando si implementa IEnumerable

L'interfaccia IEnumerable<T> ha un unico metodo, GetEnumerator() , che restituisce un IEnumerator<T> .

Mentre la parola chiave yield può essere utilizzata per creare direttamente un IEnumerable<T> , può anche essere utilizzata esattamente nello stesso modo per creare un IEnumerator<T> . L'unica cosa che cambia è il tipo di ritorno del metodo.

Questo può essere utile se vogliamo creare la nostra classe che implementa IEnumerable<T> :

public class PrintingEnumerable<T> : IEnumerable<T>
{
    private IEnumerable<T> _wrapped;

    public PrintingEnumerable(IEnumerable<T> wrapped)
    {
        _wrapped = wrapped;
    }

    // This method returns an IEnumerator<T>, rather than an IEnumerable<T>
    // But the yield syntax and usage is identical.
    public IEnumerator<T> GetEnumerator()
    {
        foreach(var item in _wrapped)
        {
            Console.WriteLine("Yielding: " + item);
            yield return item;
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

(Si noti che questo particolare esempio è solo illustrativo e potrebbe essere implementato in modo più pulito con un singolo metodo iteratore che restituisce un oggetto IEnumerable<T> .)

Stimolante valutazione

La parola chiave yield consente una valutazione lazy della collezione. Il caricamento forzato dell'intera collezione in memoria si chiama valutazione entusiasta .

Il seguente codice mostra questo:

IEnumerable<int> myMethod()
{
    for(int i=0; i <= 8675309; i++)
    {
        yield return i;
    }
}
...
// define the iterator
var it = myMethod.Take(3);
// force its immediate evaluation
// list will contain 0, 1, 2
var list = it.ToList();

Calling ToList , ToDictionary o ToArray costringono la valutazione immediata dell'enumerazione, recuperando tutti gli elementi in una raccolta.

Esempio di valutazione pigro: numeri di Fibonacci

using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics; // also add reference to System.Numberics

namespace ConsoleApplication33
{
    class Program
    {
        private static IEnumerable<BigInteger> Fibonacci()
        {
            BigInteger prev = 0;
            BigInteger current = 1;
            while (true)
            {
                yield return current;
                var next = prev + current;
                prev = current;
                current = next;
            }
        }

        static void Main()
        {
            // print Fibonacci numbers from 10001 to 10010
            var numbers = Fibonacci().Skip(10000).Take(10).ToArray();
            Console.WriteLine(string.Join(Environment.NewLine, numbers));
        }
    }
}

Come funziona sotto il cofano (consiglio di decompilare il file .exe risultante nello strumento IL Disaambler):

  1. Il compilatore C # genera una classe che implementa IEnumerable<BigInteger> e IEnumerator<BigInteger> ( <Fibonacci>d__0 in ildasm).
  2. Questa classe implementa una macchina a stati. Lo stato consiste nella posizione corrente nel metodo e nei valori delle variabili locali.
  3. Il codice più interessante si trova nel metodo bool IEnumerator.MoveNext() . Fondamentalmente, cosa MoveNext() fare:
    • Ripristina lo stato attuale. Variabili come prev e current diventano campi nella nostra classe ( <current>5__2 e <prev>5__1 in ildasm). Nel nostro metodo abbiamo due posizioni ( <>1__state ): prima alla parentesi graffa di apertura, seconda alla yield return .
    • Esegue il codice fino al prossimo yield return o yield break / } .
    • Per il valore yield return valore risultante viene salvato, quindi la proprietà Current può restituirlo. true viene restituito. A questo punto, lo stato corrente viene nuovamente salvato per la successiva MoveNext .
    • Per il metodo yield break / } restituisce solo il significato false che l'iterazione è stata eseguita.

Si noti inoltre che il 10001 ° numero è lungo 468 byte. La macchina a stati salva solo current variabili current e prev come campi. Mentre se vorremmo salvare tutti i numeri nella sequenza dal primo al 10000, la dimensione della memoria consumata sarà di oltre 4 megabyte. Quindi la valutazione pigra, se usata correttamente, può ridurre l'impronta della memoria in alcuni casi.

La differenza tra pausa e pausa di rendimento

L'uso della yield break rispetto alla break potrebbe non essere così ovvio come si potrebbe pensare. Ci sono molti cattivi esempi su Internet in cui l'uso dei due è intercambiabile e in realtà non dimostra la differenza.

La parte confusa è che entrambe le parole chiave (o frasi chiave) hanno senso solo all'interno di cicli ( foreach , while ...) Quindi quando scegliere l'una rispetto all'altra?

È importante rendersi conto che una volta che si utilizza la parola chiave yield in un metodo, si trasforma effettivamente il metodo in un iteratore . L'unico scopo di tale metodo è quindi di iterare su una collezione finita o infinita e produrre (produrre) i suoi elementi. Una volta che lo scopo è soddisfatto, non c'è motivo di continuare l'esecuzione del metodo. A volte, accade naturalmente con l'ultima parentesi di chiusura del metodo } . Ma a volte, vuoi terminare prematuramente il metodo. In un normale metodo (non iterativo) useresti la parola chiave return . Ma non puoi usare return in un iteratore, devi usare yield break . In altre parole, l' yield break per un iteratore equivale al return di un metodo standard. Mentre l'istruzione break termina semplicemente il ciclo più vicino.

Vediamo alcuni esempi:

    /// <summary>
    /// Yields numbers from 0 to 9
    /// </summary>
    /// <returns>{0,1,2,3,4,5,6,7,8,9}</returns>
    public static IEnumerable<int> YieldBreak()
    {
        for (int i = 0; ; i++)
        {
            if (i < 10)
            {
                // Yields a number
                yield return i;
            }
            else
            {
                // Indicates that the iteration has ended, everything 
                // from this line on will be ignored
                yield break;
            }
        }
        yield return 10; // This will never get executed
    }
    /// <summary>
    /// Yields numbers from 0 to 10
    /// </summary>
    /// <returns>{0,1,2,3,4,5,6,7,8,9,10}</returns>
    public static IEnumerable<int> Break()
    {
        for (int i = 0; ; i++)
        {
            if (i < 10)
            {
                // Yields a number
                yield return i;
            }
            else
            {
                // Terminates just the loop
                break;
            }
        }
        // Execution continues
        yield return 10;
    }


Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow