Was ist der Zweck / Vorteil der Verwendung von Yield Return Iteratoren in C #?

80

Alle Beispiele, die ich für die Verwendung yield return x;in einer C # -Methode gesehen habe, können auf die gleiche Weise ausgeführt werden, indem nur die gesamte Liste zurückgegeben wird. Gibt es in diesen Fällen einen Vorteil oder Vorteil bei der Verwendung der yield returnSyntax gegenüber der Rückgabe der Liste?

In welchen Szenarien würden yield returnSie nicht einfach die vollständige Liste zurückgeben können?

CoderDennis
quelle
15
Warum nehmen Sie an, dass es überhaupt "eine Liste" gibt? Was ist, wenn es keine gibt?
Eric Lippert
2
@ Eric, ich denke, das habe ich gefragt. Wann hätten Sie überhaupt keine Liste? Dateistreams und unendliche Sequenzen sind zwei gute Beispiele in den bisherigen Antworten.
CoderDennis
1
Wenn Sie die Liste dann haben, geben Sie sie einfach zurück. Wenn Sie jedoch eine Liste innerhalb der Methode erstellen und diese zurückgeben, können / sollten Sie stattdessen Iteratoren verwenden. Geben Sie die Gegenstände einzeln ab. Es gibt viele Vorteile.
justin.m.chase
2
Seit ich diese Frage vor 5 Jahren gestellt habe, habe ich sicherlich viel gelernt!
CoderDennis
1
Der größte Vorteil von yields ist, dass Sie keine weitere Zwischenvariable benennen müssen.
Nawfal

Antworten:

119

Aber was wäre, wenn Sie selbst eine Sammlung aufbauen würden?

Im Allgemeinen können Iteratoren verwendet werden, um eine Folge von Objekten träge zu erzeugen . Zum Beispiel hat die Enumerable.RangeMethode intern keine Sammlung. Bei Bedarf wird nur die nächste Nummer generiert . Es gibt viele Verwendungszwecke für diese verzögerte Sequenzgenerierung unter Verwendung einer Zustandsmaschine. Die meisten von ihnen werden unter funktionale Programmierkonzepte behandelt .

Wenn Sie Iteratoren nur als eine Möglichkeit betrachten, eine Sammlung aufzulisten (dies ist nur einer der einfachsten Anwendungsfälle), gehen Sie meiner Meinung nach den falschen Weg. Wie gesagt, Iteratoren sind Mittel zur Rückgabe von Sequenzen. Die Sequenz könnte sogar unendlich sein . Es gibt keine Möglichkeit, eine Liste mit unendlicher Länge zurückzugeben und die ersten 100 Elemente zu verwenden. Es muss manchmal faul sein. Die Rückgabe einer Sammlung unterscheidet sich erheblich von der Rückgabe eines Sammlungsgenerators (was ein Iterator ist). Es vergleicht Äpfel mit Orangen.

Hypothetisches Beispiel:

static IEnumerable<int> GetPrimeNumbers() {
   for (int num = 2; ; ++num) 
       if (IsPrime(num))
           yield return num;
}

static void Main() { 
   foreach (var i in GetPrimeNumbers()) 
       if (i < 10000)
           Console.WriteLine(i);
       else
           break;
}

In diesem Beispiel werden Primzahlen unter 10000 gedruckt. Sie können es einfach so ändern, dass Zahlen unter einer Million gedruckt werden, ohne den Algorithmus zur Generierung von Primzahlen zu berühren. In diesem Beispiel können Sie keine Liste aller Primzahlen zurückgeben, da die Reihenfolge unendlich ist und der Verbraucher von Anfang an nicht einmal weiß, wie viele Elemente er möchte.

Mehrdad Afshari
quelle
Richtig. Ich habe die Liste erstellt, aber welchen Unterschied macht es, jeweils ein Element zurückzugeben, anstatt die gesamte Liste zurückzugeben?
CoderDennis
4
Unter anderem wird Ihr Code dadurch modularer, sodass Sie ein Element laden, verarbeiten und dann wiederholen können. Betrachten Sie auch den Fall, in dem das Laden eines Artikels sehr teuer ist oder es viele davon gibt (sagen Millionen). In diesen Fällen ist das Laden der gesamten Liste unerwünscht.
Dana the Sane
15
@Dennis: Bei einer linear gespeicherten Liste im Speicher hat dies möglicherweise keinen Unterschied. Wenn Sie beispielsweise eine 10-GB-Datei aufzählen und jede Zeile einzeln verarbeiten, würde dies einen Unterschied bewirken.
Mehrdad Afshari
1
+1 für eine hervorragende Antwort - Ich möchte auch hinzufügen, dass mit dem Schlüsselwort yield die Iteratorsemantik auf Quellen angewendet werden kann, die traditionell nicht als Sammlungen betrachtet werden - wie z. B. Netzwerksockets, Webdienste oder sogar Parallelitätsprobleme (siehe stackoverflow.com/questions/). 481714 / ccr-Yield-and-Vb-Net )
LBushkin
Nettes Beispiel, also ist es im Grunde ein Sammlungsgenerator, der auf dem Kontext basiert (z. B. Methodenaufruf) und erst dann aktiv wird, wenn etwas versucht, darauf zuzugreifen, während eine herkömmliche Sammlungsmethode ohne Ertrag ihre Größe kennen müsste, um sie zu erstellen und eine vollständige Sammlung zurückgeben - dann den erforderlichen Teil dieser Sammlung durchlaufen?
Michael Harper
24

Die guten Antworten hier legen nahe, dass ein Vorteil darin yield returnbesteht, dass Sie keine Liste erstellen müssen . Listen können teuer sein. (Außerdem werden Sie sie nach einer Weile sperrig und unelegant finden.)

Aber was ist, wenn Sie keine Liste haben?

yield returnermöglicht es Ihnen, Datenstrukturen (nicht unbedingt Listen) auf verschiedene Arten zu durchlaufen . Wenn Ihr Objekt beispielsweise ein Baum ist, können Sie die Knoten vor oder nach der Reihenfolge durchlaufen, ohne andere Listen zu erstellen oder die zugrunde liegende Datenstruktur zu ändern.

public IEnumerable<T> InOrder()
{
    foreach (T k in kids)
        foreach (T n in k.InOrder())
            yield return n;
    yield return (T) this;
}

public IEnumerable<T> PreOrder()
{
    yield return (T) this;
    foreach (T k in kids)
        foreach (T n in k.PreOrder())
            yield return n;
}
Strahl
quelle
1
Dieses Beispiel zeigt auch den Fall der Delegierung. Wenn Sie eine Sammlung haben, die unter bestimmten Umständen die Elemente anderer Sammlungen enthalten kann, ist es sehr einfach, die Ertragsrückgabe zu iterieren und zu verwenden, anstatt eine vollständige Liste aller Ergebnisse zu erstellen und diese zurückzugeben.
Tom Mayfield
1
Jetzt muss C # nur yield!noch so implementieren wie F #, damit Sie nicht alle foreachAnweisungen benötigen .
CoderDennis
Ihr Beispiel zeigt übrigens eine der "Gefahren" von yield return: Es ist oft nicht offensichtlich, wann es effizienten oder ineffizienten Code liefert. Obwohl yield returneine solche Verwendung rekursiv verwendet werden kann, bedeutet dies eine erhebliche Belastung für die Verarbeitung tief verschachtelter Enumeratoren. Die manuelle Statusverwaltung ist möglicherweise komplizierter zu codieren, läuft jedoch viel effizienter.
Supercat
16

Lazy Evaluation / Aufgeschobene Ausführung

Die Iteratorblöcke "Yield Return" führen keinen Code aus, bis Sie tatsächlich dieses bestimmte Ergebnis aufrufen. Dies bedeutet, dass sie auch effizient miteinander verkettet werden können. Pop-Quiz: Wie oft wird der folgende Code über die Datei iteriert?

var query = File.ReadLines(@"C:\MyFile.txt")
                            .Where(l => l.Contains("search text") )
                            .Select(l => int.Parse(l.SubString(5,8))
                            .Where(i => i > 10 );

int sum=0;
foreach (int value in query) 
{
    sum += value;
}

Die Antwort ist genau eine, und das erst ganz unten in der foreachSchleife. Obwohl ich drei separate Linq-Operatorfunktionen habe, durchlaufen wir den Inhalt der Datei nur einmal.

Dies hat andere Vorteile als die Leistung. Zum Beispiel kann ich eine ziemlich einfache und generische Methode schreiben, um eine Protokolldatei einmal zu lesen und vorzufiltern, und dieselbe Methode an mehreren verschiedenen Stellen verwenden, wobei jede Verwendung unterschiedliche Filter hinzufügt. Auf diese Weise kann ich eine gute Leistung erzielen und gleichzeitig Code effizient wiederverwenden.

Unendliche Listen

In meiner Antwort auf diese Frage finden Sie ein gutes Beispiel: Die
C # -Fibonacci-Funktion gibt Fehler zurück

Grundsätzlich implementiere ich die Fibonacci-Sequenz mit einem Iteratorblock, der niemals stoppt (zumindest nicht vor Erreichen von MaxInt), und verwende diese Implementierung dann auf sichere Weise.

Verbesserte Semantik und Trennung von Bedenken

Anhand des obigen Dateibeispiels können wir jetzt den Code, der die Datei liest, einfach von dem Code trennen, der nicht benötigte Zeilen aus dem Code herausfiltert, der die Ergebnisse tatsächlich analysiert. Insbesondere dieser erste ist sehr wiederverwendbar.

Dies ist eines der Dinge, die mit Prosa viel schwerer zu erklären sind als nur mit einem einfachen visuellen 1 :

Imperative vs funktionale Trennung von Bedenken

Wenn Sie das Bild nicht sehen können, werden zwei Versionen desselben Codes mit Hintergrundhighlights für verschiedene Probleme angezeigt. Der Linq-Code hat alle Farben schön gruppiert, während der traditionelle Imperativ-Code die Farben vermischt hat. Der Autor argumentiert (und ich stimme zu), dass dieses Ergebnis typisch für die Verwendung von linq im Vergleich zur Verwendung von imperativem Code ist ... dass linq Ihren Code besser organisiert, um einen besseren Fluss zwischen Abschnitten zu erzielen.


1 Ich glaube, dies ist die Originalquelle: https://twitter.com/mariofusco/status/571999216039542784 . Beachten Sie auch, dass dieser Code Java ist, aber das C # ähnlich wäre.

Joel Coehoorn
quelle
1
Die verzögerte Ausführung ist wahrscheinlich der größte Vorteil von Iteratoren.
justin.m.chase
12

Manchmal sind die Sequenzen, die Sie zurückgeben müssen, einfach zu groß, um in den Speicher zu passen. Zum Beispiel habe ich vor ungefähr 3 Monaten an einem Projekt zur Datenmigration zwischen MS SLQ-Datenbanken teilgenommen. Die Daten wurden im XML-Format exportiert. Die Rendite war bei XmlReader sehr nützlich . Das hat die Programmierung sehr erleichtert. Angenommen, eine Datei enthält 1000 Kundenelemente. Wenn Sie diese Datei nur in den Speicher lesen, müssen Sie alle gleichzeitig im Speicher speichern, auch wenn sie nacheinander verarbeitet werden. Sie können also Iteratoren verwenden, um die Sammlung einzeln zu durchlaufen. In diesem Fall müssen Sie nur Speicher für ein Element ausgeben.

Wie sich herausstellte, war die Verwendung von XmlReader für unser Projekt die einzige Möglichkeit, die Anwendung zum Laufen zu bringen - es hat lange funktioniert, aber zumindest hat es nicht das gesamte System hängen lassen und keine OutOfMemoryException ausgelöst . Natürlich können Sie mit XmlReader ohne Ertragsiteratoren arbeiten . Aber Iteratoren haben mein Leben viel einfacher gemacht (ich würde den Code für den Import nicht so schnell und ohne Probleme schreiben). Sehen Sie sich diese Seite an, um zu sehen, wie Ertragsiteratoren zur Lösung realer Probleme verwendet werden (nicht nur wissenschaftlich mit unendlichen Sequenzen).

SPIRiT_1984
quelle
9

In Spielzeug- / Demonstrationsszenarien gibt es keinen großen Unterschied. Es gibt jedoch Situationen, in denen es nützlich ist, Iteratoren auszugeben - manchmal ist nicht die gesamte Liste verfügbar (z. B. Streams) oder die Liste ist rechenintensiv und wird wahrscheinlich nicht vollständig benötigt.

Dan Davies Brackett
quelle
2

Wenn die gesamte Liste gigantisch ist, verbraucht sie möglicherweise viel Speicher, nur um herumzusitzen, während Sie mit dem Ertrag nur mit dem spielen, was Sie brauchen, wenn Sie es brauchen, unabhängig davon, wie viele Elemente es gibt.

Nilamo
quelle
2

Mit yield returnkönnen Sie Elemente durchlaufen, ohne jemals eine Liste erstellen zu müssen. Wenn Sie die Liste nicht benötigen, aber einige Elemente durchlaufen möchten, ist das Schreiben möglicherweise einfacher

foreach (var foo in GetSomeFoos()) {
    operate on foo
}

Als

foreach (var foo in AllFoos) {
    if (some case where we do want to operate on foo) {
        operate on foo
    } else if (another case) {
        operate on foo
    }
}

Sie können die gesamte Logik für die Bestimmung, ob Sie mit foo arbeiten möchten, in Ihre Methode einfügen, indem Sie Ertragsrenditen verwenden, und Sie können jede Schleife viel präziser gestalten.

AgileJon
quelle
2

Hier ist meine zuvor akzeptierte Antwort auf genau dieselbe Frage:

Rendite Keyword Mehrwert?

Eine andere Möglichkeit, Iteratormethoden zu betrachten, besteht darin, dass sie die harte Arbeit leisten, einen Algorithmus "von innen nach außen" zu drehen. Betrachten Sie einen Parser. Es zieht Text aus einem Stream, sucht darin nach Mustern und generiert eine logische Beschreibung des Inhalts auf hoher Ebene.

Jetzt kann ich es mir als Parserautor leicht machen, indem ich den SAX-Ansatz verwende, bei dem ich eine Rückrufschnittstelle habe, die ich benachrichtige, wenn ich das nächste Stück des Musters finde. Im Fall von SAX rufe ich jedes Mal, wenn ich den Anfang eines Elements finde, die beginElementMethode auf und so weiter.

Dies schafft jedoch Probleme für meine Benutzer. Sie müssen die Handler-Schnittstelle implementieren und daher eine Zustandsmaschinenklasse schreiben, die auf die Rückrufmethoden reagiert. Dies ist schwer zu korrigieren, daher ist es am einfachsten, eine Standardimplementierung zu verwenden, die einen DOM-Baum erstellt, und dann haben sie die Möglichkeit, über den Baum zu gehen. Aber dann wird die gesamte Struktur im Speicher gepuffert - nicht gut.

Aber wie wäre es stattdessen, wenn ich meinen Parser als Iteratormethode schreibe?

IEnumerable<LanguageElement> Parse(Stream stream)
{
    // imperative code that pulls from the stream and occasionally 
    // does things like:

    yield return new BeginStatement("if");

    // and so on...
}

Das ist nicht schwieriger zu schreiben als der Callback-Interface-Ansatz - geben Sie einfach ein von meiner LanguageElementBasisklasse abgeleitetes Objekt zurück, anstatt eine Callback-Methode aufzurufen.

Der Benutzer kann jetzt foreach verwenden, um die Ausgabe meines Parsers zu durchlaufen, sodass er eine sehr praktische, zwingende Programmierschnittstelle erhält.

Das Ergebnis ist, dass beide Seiten einer benutzerdefinierten API so aussehen, als hätten sie die Kontrolle und sind daher einfacher zu schreiben und zu verstehen.

Daniel Earwicker
quelle
2

Der Hauptgrund für die Verwendung von Yield ist, dass eine Liste selbst generiert / zurückgegeben wird. Wir können die zurückgegebene Liste verwenden, um weiter zu iterieren.

Tapas Ranjan Singh
quelle
Konzeptionell korrekt, aber technisch falsch. Es gibt eine Instanz von IEnumerable zurück, die einfach einen Iterator abstrahiert. Dieser Iterator ist eigentlich logisch, um das nächste Element und keine materialisierte Liste zu erhalten. Mit return yieldwird keine Liste generiert, sondern nur das nächste Element in der Liste und nur auf Anfrage (iteriert).
Sinaesthetic