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 return
Syntax gegenüber der Rückgabe der Liste?
In welchen Szenarien würden yield return
Sie nicht einfach die vollständige Liste zurückgeben können?
c#
iterator
yield-return
CoderDennis
quelle
quelle
yield
s ist, dass Sie keine weitere Zwischenvariable benennen müssen.Antworten:
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.Range
Methode 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.
quelle
Die guten Antworten hier legen nahe, dass ein Vorteil darin
yield return
besteht, 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 return
ermö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; }
quelle
yield!
noch so implementieren wie F #, damit Sie nicht alleforeach
Anweisungen benötigen .yield return
: Es ist oft nicht offensichtlich, wann es effizienten oder ineffizienten Code liefert. Obwohlyield return
eine 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.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
foreach
Schleife. 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 :
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.
quelle
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).
quelle
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.
quelle
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.
quelle
Werfen Sie einen Blick auf diese Diskussion in Eric Whites Blog (übrigens ein ausgezeichneter Blog) über faule versus eifrige Bewertungen .
quelle
Mit
yield return
kö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 einfacherforeach (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.
quelle
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
beginElement
Methode 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
LanguageElement
Basisklasse 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.
quelle
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.
quelle
return yield
wird keine Liste generiert, sondern nur das nächste Element in der Liste und nur auf Anfrage (iteriert).