Perché usare la parola chiave yield quando potrei semplicemente usare un normale IEnumerable?


171

Dato questo codice:

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

Perché non dovrei semplicemente codificarlo in questo modo ?:

IEnumerable<object> FilteredList()
{
    var list = new List<object>(); 
    foreach( object item in FullList )
    {
        if( IsItemInPartialList( item ) )
            list.Add(item);
    }
    return list;
}

Ho capito cosa fa la yieldparola chiave. Indica al compilatore di creare un certo tipo di cose (un iteratore). Ma perché usarlo? A parte il fatto che è leggermente meno codice, cosa fa per me?


28
Mi rendo conto che questo è solo un esempio, ma in realtà, il codice dovrebbe essere scritto in questo modo: FullList.Where(IsItemInPartialList):)
BlueRaja - Danny Pflughoeft

Risposte:


241

L'utilizzo yieldrende la raccolta pigra.

Diciamo che hai solo bisogno dei primi cinque elementi. A modo tuo, devo scorrere l' intero elenco per ottenere i primi cinque elementi. Con yield, cerco solo i primi cinque elementi.


14
Si noti che l'utilizzo FullList.Where(IsItemInPartialList)sarà altrettanto pigro. Solo, richiede molto meno codice --- gunk --- personalizzato generato dal compilatore. E meno tempo per gli sviluppatori a scrivere e gestire. (Certo, quello era proprio questo esempio)
vedi il

4
Quello è Linq, vero? Immagino che Linq faccia qualcosa di molto simile sotto le coperte.
Robert Harvey,

1
Sì, Linq ha usato l'esecuzione ritardata ( yield return) dove mai possibile.
Chad Schouggins,

11
Non dimenticare che se l'istruzione return return non viene mai eseguita, si ottiene comunque un risultato di raccolta vuoto, quindi non è necessario preoccuparsi di un'eccezione di riferimento null. il rendimento è fantastico con granelli di cioccolato.
Rasoio

127

Il vantaggio dei blocchi iteratori è che funzionano pigramente. Quindi puoi scrivere un metodo di filtro come questo:

public static IEnumerable<T> Where<T>(this IEnumerable<T> source,
                                   Func<T, bool> predicate)
{
    foreach (var item in source)
    {
        if (predicate(item))
        {
            yield return item;
        }
    }
}

Ciò ti consentirà di filtrare uno stream per tutto il tempo che desideri, senza mai eseguire il buffering di più di un singolo elemento alla volta. Se hai bisogno solo del primo valore della sequenza restituita, ad esempio, perché dovresti voler copiare tutto in un nuovo elenco?

Come altro esempio, puoi facilmente creare un flusso infinito usando i blocchi iteratori. Ad esempio, ecco una sequenza di numeri casuali:

public static IEnumerable<int> RandomSequence(int minInclusive, int maxExclusive)
{
    Random rng = new Random();
    while (true)
    {
        yield return rng.Next(minInclusive, maxExclusive);
    }
}

Come memorizzeresti una sequenza infinita in un elenco?

La mia serie di blog su Edulinq fornisce un'implementazione di esempio di LINQ to Objects che fa un uso intenso dei blocchi iteratori. LINQ è fondamentalmente pigro dove può essere - e mettere le cose in un elenco semplicemente non funziona in questo modo.


1
Non sono sicuro che ti piaccia RandomSequenceo no. Per me, IEnumerable significa - prima di tutto - che posso iterare con foreach, ma questo ovviamente porterebbe a un ciclo infinito qui. Considererei questo un uso piuttosto pericoloso del concetto IEnumerable, ma YMMV.
Sebastian Negraszus,

5
@SebastianNegraszus: una sequenza di numeri casuali è logicamente infinita. Ad esempio, potresti creare facilmente una IEnumerable<BigInteger>rappresentazione della sequenza di Fibonacci. Puoi usarlo foreach, ma nulla IEnumerable<T>garantisce che sarà finito.
Jon Skeet,

42

Con il codice "elenco", è necessario elaborare l'elenco completo prima di poterlo passare al passaggio successivo. La versione "yield" passa immediatamente l'articolo elaborato al passaggio successivo. Se quel "passo successivo" contiene un ".Take (10)", la versione "yield" elaborerà solo i primi 10 elementi e dimenticherà il resto. Il codice "list" avrebbe elaborato tutto.

Ciò significa che vedi la differenza maggiore quando devi fare molta elaborazione e / o avere lunghi elenchi di elementi da elaborare.


23

È possibile utilizzare yieldper restituire elementi che non sono in un elenco. Ecco un piccolo esempio che potrebbe scorrere all'infinito attraverso un elenco fino alla cancellazione.

public IEnumerable<int> GetNextNumber()
{
    while (true)
    {
        for (int i = 0; i < 10; i++)
        {
            yield return i;
        }
    }
}

public bool Canceled { get; set; }

public void StartCounting()
{
    foreach (var number in GetNextNumber())
    {
        if (this.Canceled) break;
        Console.WriteLine(number);
    }
}

Questo scrive

0
1
2
3
4
5
6
7
8
9
0
1
2
3
4

...eccetera. alla console fino a quando non viene annullato.


10
object jamesItem = null;
foreach(var item in FilteredList())
{
   if (item.Name == "James")
   {
       jamesItem = item;
       break;
   }
}
return jamesItem;

Quando si utilizza il codice sopra per scorrere FilteredList () e supponendo item.Name == "James" sarà soddisfatto sul 2 ° elemento dell'elenco, il metodo utilizzato yieldprodurrà due volte. Questo è un comportamento pigro.

Dove come metodo usando la lista aggiungerà tutti gli n oggetti alla lista e passerà la lista completa al metodo chiamante.

Questo è esattamente un caso d'uso in cui è possibile evidenziare la differenza tra IEnumerable e IList.


7

Il miglior esempio del mondo reale che ho visto per l'uso yieldsarebbe quello di calcolare una sequenza di Fibonacci.

Considera il seguente codice:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(string.Join(", ", Fibonacci().Take(10)));
        Console.WriteLine(string.Join(", ", Fibonacci().Skip(15).Take(1)));
        Console.WriteLine(string.Join(", ", Fibonacci().Skip(10).Take(5)));
        Console.WriteLine(string.Join(", ", Fibonacci().Skip(100).Take(1)));
        Console.ReadKey();
    }

    private static IEnumerable<long> Fibonacci()
    {
        long a = 0;
        long b = 1;

        while (true)
        {
            long temp = a;
            a = b;

            yield return a;

            b = temp + b;
        }
    }
}

Questo restituirà:

1, 1, 2, 3, 5, 8, 13, 21, 34, 55
987
89, 144, 233, 377, 610
1298777728820984005

Questo è utile perché ti consente di calcolare una serie infinita in modo rapido e semplice, dandoti la possibilità di utilizzare le estensioni Linq e interrogare solo ciò di cui hai bisogno.


5
Non riesco a vedere nulla di "mondo reale" in un calcolo della sequenza di Fibonacci.
Nek,

Concordo sul fatto che questo non è davvero "mondo reale", ma che bella idea.
Casey

1

perché usare [rendimento]? A parte il fatto che è leggermente meno codice, cosa fa per me?

A volte è utile, a volte no. Se l'intero insieme di dati deve essere esaminato e restituito, non ci sarà alcun vantaggio nell'utilizzare il rendimento perché tutto ciò che ha fatto è stato introdurre un sovraccarico.

Quando il rendimento brilla davvero è quando viene restituito solo un set parziale. Penso che il miglior esempio sia l'ordinamento. Supponi di avere un elenco di oggetti che contengono una data e un importo in dollari di quest'anno e che vorresti vedere i primi (5) record dell'anno.

Per fare ciò, l'elenco deve essere ordinato in ordine crescente per data e quindi prendere i primi 5. Se ciò fosse fatto senza rendimento, l' intero elenco dovrebbe essere ordinato, fino ad accertarsi che le ultime due date fossero in ordine.

Tuttavia, con la resa, una volta stabiliti i primi 5 articoli, l'ordinamento si interrompe e i risultati sono disponibili. Ciò può far risparmiare molto tempo.


0

L'istruzione return return consente di restituire solo un articolo alla volta. Stai raccogliendo tutti gli elementi in un elenco e restituendo nuovamente tale elenco, che è un sovraccarico di memoria.

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.