Warum das Schlüsselwort yield verwenden, wenn ich nur eine normale IEnumerable verwenden könnte?

171

Angesichts dieses Codes:

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

Warum sollte ich es nicht einfach so codieren?:

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

Ich verstehe irgendwie, was das yieldSchlüsselwort bewirkt. Es weist den Compiler an, eine bestimmte Art von Dingen (einen Iterator) zu erstellen. Aber warum es benutzen? Was macht es für mich, abgesehen davon, dass es etwas weniger Code ist?

James P. Wright
quelle
28
Ich weiß , dies ist nur ein Beispiel, aber wirklich, der Code sollte wie folgt geschrieben werden: FullList.Where(IsItemInPartialList):)
BlueRaja - Danny Pflughoeft
In Verbindung stehender Beitrag - Wofür wird das Ertragsschlüsselwort in C # verwendet?
RBT

Antworten:

241

Die Verwendung yieldmacht die Sammlung faul.

Angenommen, Sie benötigen nur die ersten fünf Elemente. Auf Ihre Weise muss ich die gesamte Liste durchlaufen , um die ersten fünf Elemente zu erhalten. Mit yieldschleife ich nur die ersten fünf Elemente durch.

Robert Harvey
quelle
14
Beachten Sie, dass die Verwendung FullList.Where(IsItemInPartialList)genauso faul ist. Nur erfordert es weit weniger vom Compiler generierten benutzerdefinierten --- Gunk --- Code. Und weniger Entwicklerzeit beim Schreiben und Verwalten. (Natürlich war das nur dieses Beispiel)
sehe
4
Das ist doch Linq, oder? Ich stelle mir vor, dass Linq unter der Decke etwas sehr Ähnliches macht.
Robert Harvey
1
Ja, Linq hat, yield returnwo immer möglich , die verzögerte Ausführung ( ) verwendet.
Chad Schouggins
11
Vergessen Sie nicht, dass Sie immer noch ein leeres Erfassungsergebnis erhalten, wenn die Yield-Return-Anweisung nie ausgeführt wird, sodass Sie sich keine Gedanken über eine Nullreferenzausnahme machen müssen. Ertragsrendite ist mit Schokostreuseln fantastisch.
Rasiermesser
127

Der Vorteil von Iteratorblöcken besteht darin, dass sie träge arbeiten. Sie können also eine Filtermethode wie folgt schreiben:

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

Auf diese Weise können Sie einen Stream so lange filtern, wie Sie möchten, und niemals mehr als ein einzelnes Element gleichzeitig puffern. Wenn Sie beispielsweise nur den ersten Wert aus der zurückgegebenen Sequenz benötigen, warum sollten Sie dann alles in eine neue Liste kopieren ?

Als weiteres Beispiel können Sie mit Iteratorblöcken einfach einen unendlichen Stream erstellen . Hier ist zum Beispiel eine Folge von Zufallszahlen:

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

Wie würden Sie eine unendliche Sequenz in einer Liste speichern?

Meine Edulinq Blog - Serie gibt eine Beispielimplementierung von LINQ to Objects , die macht schwere Verwendung von Iterator Blöcke. LINQ ist grundsätzlich faul, wo es sein kann - und Dinge in eine Liste aufzunehmen, funktioniert einfach nicht so.

Jon Skeet
quelle
1
Ich bin mir nicht sicher, ob ich deine mögen soll RandomSequenceoder nicht. IEnumerable bedeutet für mich in erster Linie, dass ich mit foreach iterieren kann, aber dies würde hier offensichtlich zu einer unendlichen Schleife führen. Ich würde dies als einen ziemlich gefährlichen Missbrauch des IEnumerable-Konzepts betrachten, aber YMMV.
Sebastian Negraszus
5
@SebastianNegraszus: Eine Folge von Zufallszahlen ist logisch unendlich. Sie können beispielsweise leicht eine IEnumerable<BigInteger>Darstellung der Fibonacci-Sequenz erstellen . Sie können foreaches verwenden, aber nichts über IEnumerable<T>garantiert, dass es endlich sein wird.
Jon Skeet
42

Mit dem "Listen" -Code müssen Sie die vollständige Liste verarbeiten, bevor Sie sie an den nächsten Schritt weitergeben können. Die "Yield" -Version übergibt den verarbeiteten Artikel sofort an den nächsten Schritt. Wenn dieser "nächste Schritt" ein ".Take (10)" enthält, verarbeitet die "Yield" -Version nur die ersten 10 Elemente und vergisst den Rest. Der "Listen" -Code hätte alles verarbeitet.

Dies bedeutet, dass Sie den größten Unterschied sehen, wenn Sie viel verarbeiten müssen und / oder lange Listen mit zu verarbeitenden Elementen haben.

Hans Keing
quelle
23

Sie können yieldElemente zurückgeben, die nicht in einer Liste enthalten sind. Hier ist ein kleines Beispiel, das eine Liste unendlich durchlaufen kann, bis es abgebrochen wird.

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

Das schreibt

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

...etc. an die Konsole bis zum Abbruch.

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

Wenn der obige Code zum Durchlaufen von FilteredList () verwendet wird und davon ausgegangen wird, dass item.Name == "James" für das zweite Element in der Liste erfüllt ist, ergibt die verwendete Methode yieldzweimal. Dies ist ein faules Verhalten.

Wobei als Methode mit Liste alle n Objekte zur Liste hinzugefügt und die vollständige Liste an die aufrufende Methode übergeben werden.

Dies ist genau ein Anwendungsfall, bei dem der Unterschied zwischen IEnumerable und IList hervorgehoben werden kann.

Hummelistener
quelle
7

Das beste Beispiel aus der realen Welt, das ich für die Verwendung gesehen habe, yieldwäre die Berechnung einer Fibonacci-Sequenz.

Betrachten Sie den folgenden Code:

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

Dies wird zurückkehren:

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

Dies ist hilfreich, da Sie so schnell und einfach eine unendliche Reihe berechnen können, sodass Sie die Linq-Erweiterungen verwenden und nur das abfragen können, was Sie benötigen.

Middas
quelle
5
Ich kann in einer Fibonacci-Sequenzberechnung nichts "Reales" sehen.
Nek
Ich stimme zu, dass dies nicht wirklich "real" ist, aber was für eine coole Idee.
Casey
1

warum [Ausbeute] verwenden? Was macht es für mich, abgesehen davon, dass es etwas weniger Code ist?

Manchmal ist es nützlich, manchmal nicht. Wenn der gesamte Datensatz geprüft und zurückgegeben werden muss, hat die Verwendung von Yield keinen Vorteil, da lediglich der Overhead eingeführt wurde.

Wenn der Ertrag wirklich glänzt, wird nur ein Teilsatz zurückgegeben. Ich denke, das beste Beispiel ist das Sortieren. Angenommen, Sie haben eine Liste von Objekten, die ein Datum und einen Dollarbetrag aus diesem Jahr enthalten, und Sie möchten die ersten wenigen (5) Datensätze des Jahres sehen.

Um dies zu erreichen, muss die Liste nach Datum aufsteigend sortiert werden und dann müssen die ersten 5 genommen werden. Wenn dies ohne Ertrag erfolgen würde, müsste die gesamte Liste sortiert werden, um sicherzustellen, dass die letzten beiden Daten in Ordnung sind.

Mit der Ausbeute stoppt die Sortierung jedoch, sobald die ersten 5 Elemente festgelegt wurden, und die Ergebnisse sind verfügbar. Dies kann viel Zeit sparen.

Travis J.
quelle
0

Mit der Yield Return-Anweisung können Sie jeweils nur einen Artikel zurückgeben. Sie sammeln alle Elemente in einer Liste und geben diese Liste erneut zurück. Dies ist ein Speicheraufwand.

Prabhavith
quelle