Richtige Verwendung von 'Yield Return'


903

Das Yield- Schlüsselwort ist eines dieser Schlüsselwörter in C #, das mich immer wieder mystifiziert, und ich war mir nie sicher, ob ich es richtig verwende.

Welcher der beiden folgenden Codeteile ist der bevorzugte und warum?

Version 1: Ertragsrendite verwenden

public static IEnumerable<Product> GetAllProducts()
{
    using (AdventureWorksEntities db = new AdventureWorksEntities())
    {
        var products = from product in db.Product
                       select product;

        foreach (Product product in products)
        {
            yield return product;
        }
    }
}

Version 2: Geben Sie die Liste zurück

public static IEnumerable<Product> GetAllProducts()
{
    using (AdventureWorksEntities db = new AdventureWorksEntities())
    {
        var products = from product in db.Product
                       select product;

        return products.ToList<Product>();
    }
}

38
yieldist gebunden IEnumerable<T>und seine Art. Es ist in einer irgendwie faulen Bewertung
Jaider

Hier ist eine großartige Antwort auf die ähnliche Frage. stackoverflow.com/questions/15381708/…
Sanjeev Rai

1
Hier ist ein gutes Anwendungsbeispiel: stackoverflow.com/questions/3392612/…
ValGe

6
Ich sehe einen guten Fall für die Verwendung, yield returnwenn der Code, der die Ergebnisse von durchläuft, GetAllProducts()dem Benutzer die Möglichkeit gibt, die Verarbeitung vorzeitig abzubrechen.
JMD

2
Ich fand diesen Thread wirklich hilfreich: programmers.stackexchange.com/a/97350/148944
PiotrWolkowski

Antworten:


806

Ich neige dazu, die Rendite zu verwenden, wenn ich den nächsten Artikel in der Liste (oder sogar die nächste Gruppe von Artikeln) berechne.

Bei Verwendung Ihrer Version 2 müssen Sie über die vollständige Liste verfügen, bevor Sie zurückkehren können. Wenn Sie Yield-Return verwenden, müssen Sie wirklich nur den nächsten Artikel haben, bevor Sie zurückkehren.

Dies hilft unter anderem dabei, die Berechnungskosten komplexer Berechnungen über einen größeren Zeitraum zu verteilen. Wenn die Liste beispielsweise mit einer GUI verbunden ist und der Benutzer niemals zur letzten Seite wechselt, berechnen Sie niemals die endgültigen Elemente in der Liste.

Ein anderer Fall, in dem Rendite-Rendite vorzuziehen ist, ist, wenn IEnumerable eine unendliche Menge darstellt. Betrachten Sie die Liste der Primzahlen oder eine unendliche Liste von Zufallszahlen. Sie können niemals die vollständige IEnumerable auf einmal zurückgeben, daher verwenden Sie Yield-Return, um die Liste schrittweise zurückzugeben.

In Ihrem speziellen Beispiel haben Sie die vollständige Liste der Produkte, daher würde ich Version 2 verwenden.


31
Ich würde nicht sagen, dass in Ihrem Beispiel in Frage 3 zwei Vorteile miteinander verbunden sind. 1) Es verteilt die Rechenkosten (manchmal ein Vorteil, manchmal nicht). 2) In vielen Anwendungsfällen kann die Berechnung auf unbestimmte Zeit verzögert vermieden werden. Sie erwähnen nicht den möglichen Nachteil, den es im Zwischenzustand hat. Wenn Sie erhebliche Mengen an Zwischenzuständen haben (z. B. ein HashSet zur Eliminierung von Duplikaten), kann die Verwendung von Yield Ihren Speicherbedarf erhöhen.
Kennet Belenky

8
Wenn jedes einzelne Element sehr groß ist, aber nur nacheinander zugegriffen werden muss, ist die Ausbeute besser.
Kennet Belenky

2
Und schließlich ... gibt es eine etwas wackelige, aber gelegentlich effektive Technik, um mithilfe von Yield asynchronen Code in einer sehr serialisierten Form zu schreiben.
Kennet Belenky

12
Ein weiteres interessantes Beispiel ist das Lesen ziemlich großer CSV-Dateien. Sie möchten jedes Element lesen, aber auch Ihre Abhängigkeit entfernen. Wenn Sie eine IEnumerable <> zurückgeben, können Sie jede Zeile zurückgeben und jede Zeile einzeln verarbeiten. Sie müssen keine 10-MB-Datei in den Speicher lesen. Nur eine Zeile zu einer Zeit.
Maxime Rouiller

1
Yield returnscheint eine Abkürzung für das Schreiben einer eigenen benutzerdefinierten Iterator-Klasse zu sein (IEnumerator implementieren). Daher gelten die genannten Vorteile auch für benutzerdefinierte Iteratorklassen. Auf jeden Fall behalten beide Konstrukte den Zwischenzustand bei. In seiner einfachsten Form geht es darum, einen Verweis auf das aktuelle Objekt zu halten.
J. Ouwehand

641

Das Auffüllen einer temporären Liste ist wie das Herunterladen des gesamten Videos, während das Verwenden yieldwie das Streamen dieses Videos ist.


180
Ich bin mir vollkommen bewusst, dass diese Antwort keine technische Antwort ist, aber ich glaube, dass die Ähnlichkeit zwischen Ertrag und Video-Streaming ein gutes Beispiel für das Verständnis des Ertragsschlüsselworts ist. Alles Technische wurde bereits zu diesem Thema gesagt, deshalb habe ich versucht, "mit anderen Worten" zu erklären. Gibt es eine Community-Regel, die besagt, dass Sie Ihre Ideen nicht technisch erklären können?
Anar Khalilov

13
Ich bin mir nicht sicher, wer Sie herabgestimmt hat oder warum (ich wünschte, sie hätten dies kommentiert), aber ich denke, es beschreibt es etwas aus einer nicht-technischen Perspektive.
Senfo

22
Das Konzept immer noch zu verstehen und dies half, es weiter in den Fokus zu rücken, eine schöne Analogie.
Tony

11
Ich mag diese Antwort, aber sie beantwortet die Frage nicht.
ANeves

73

Nehmen yieldwir als konzeptionelles Beispiel für das Verständnis, wann Sie verwenden sollten, an , dass die Methode ConsumeLoop()die zurückgegebenen / zurückgegebenen Elemente verarbeitet von ProduceList():

void ConsumeLoop() {
    foreach (Consumable item in ProduceList())        // might have to wait here
        item.Consume();
}

IEnumerable<Consumable> ProduceList() {
    while (KeepProducing())
        yield return ProduceExpensiveConsumable();    // expensive
}

Ohne yieldkann der Anruf bei ProduceList()lange dauern, da Sie die Liste vervollständigen müssen, bevor Sie zurückkehren:

//pseudo-assembly
Produce consumable[0]                   // expensive operation, e.g. disk I/O
Produce consumable[1]                   // waiting...
Produce consumable[2]                   // waiting...
Produce consumable[3]                   // completed the consumable list
Consume consumable[0]                   // start consuming
Consume consumable[1]
Consume consumable[2]
Consume consumable[3]

Mit yieldwird es neu angeordnet und arbeitet "parallel":

//pseudo-assembly
Produce consumable[0]
Consume consumable[0]                   // immediately Consume
Produce consumable[1]
Consume consumable[1]                   // consume next
Produce consumable[2]
Consume consumable[2]                   // consume next
Produce consumable[3]
Consume consumable[3]                   // consume next

Und schließlich sollten Sie, wie viele bereits vorgeschlagen haben, Version 2 verwenden, da Sie die fertige Liste ohnehin schon haben.


30

Ich weiß, dass dies eine alte Frage ist, aber ich möchte ein Beispiel dafür anbieten, wie das Yield-Schlüsselwort kreativ verwendet werden kann. Ich habe wirklich von dieser Technik profitiert. Hoffentlich hilft dies allen anderen, die über diese Frage stolpern.

Hinweis: Stellen Sie sich das Yield-Schlüsselwort nicht nur als eine andere Möglichkeit zum Erstellen einer Sammlung vor. Ein großer Teil der Ertragskraft besteht darin, dass die Ausführung in Ihrer Methode oder Eigenschaft angehalten wird, bis der aufrufende Code über den nächsten Wert iteriert. Hier ist mein Beispiel:

Die Verwendung des Yield-Schlüsselworts (neben Rob Eisenburgs Implementierung von Caliburn.Micro Coroutines ) ermöglicht es mir, einen asynchronen Aufruf an einen Webdienst wie diesen auszudrücken:

public IEnumerable<IResult> HandleButtonClick() {
    yield return Show.Busy();

    var loginCall = new LoginResult(wsClient, Username, Password);
    yield return loginCall;
    this.IsLoggedIn = loginCall.Success;

    yield return Show.NotBusy();
}

Dadurch wird mein BusyIndicator aktiviert, die Anmeldemethode in meinem Webdienst aufgerufen, mein IsLoggedIn-Flag auf den Rückgabewert gesetzt und dann der BusyIndicator wieder deaktiviert.

So funktioniert das: IResult verfügt über eine Execute-Methode und ein Completed-Ereignis. Caliburn.Micro greift den IEnumerator aus dem Aufruf von HandleButtonClick () ab und übergibt ihn an eine Coroutine.BeginExecute-Methode. Die BeginExecute-Methode beginnt mit der Iteration durch die IResults. Wenn das erste IResult zurückgegeben wird, wird die Ausführung in HandleButtonClick () angehalten, und BeginExecute () fügt dem Ereignis Completed einen Ereignishandler hinzu und ruft Execute () auf. IResult.Execute () kann entweder eine synchrone oder eine asynchrone Aufgabe ausführen und löst das Ereignis "Abgeschlossen" aus, wenn es abgeschlossen ist.

LoginResult sieht ungefähr so ​​aus:

public LoginResult : IResult {
    // Constructor to set private members...

    public void Execute(ActionExecutionContext context) {
        wsClient.LoginCompleted += (sender, e) => {
            this.Success = e.Result;
            Completed(this, new ResultCompletionEventArgs());
        };
        wsClient.Login(username, password);
    }

    public event EventHandler<ResultCompletionEventArgs> Completed = delegate { };
    public bool Success { get; private set; }
}

Es kann hilfreich sein, so etwas einzurichten und die Ausführung zu durchlaufen, um zu beobachten, was los ist.

Hoffe das hilft jemandem! Ich habe es wirklich genossen, die verschiedenen Verwendungsmöglichkeiten von Erträgen zu erkunden.


1
Ihr Codebeispiel ist ein hervorragendes Beispiel für die Verwendung der Ausbeute AUSSERHALB eines for- oder foreach-Blocks. Die meisten Beispiele zeigen die Rendite innerhalb eines Iterators. Sehr hilfreich, da ich gerade die Frage zu SO stellen wollte, wie man Yield außerhalb eines Iterators verwendet!
Shelbypereira

Es ist mir nie in den Sinn gekommen, yieldauf diese Weise zu verwenden. Es scheint eine elegante Möglichkeit zu sein, das Async / Wait-Muster zu emulieren (von dem ich annehme, dass es verwendet wird, anstatt yieldwenn es heute neu geschrieben würde). Haben Sie festgestellt, dass diese kreativen Verwendungen vonyield (kein Wortspiel beabsichtigt) im Laufe der Jahre zu sinkenden Renditen geführt haben, da sich C # seit der Beantwortung dieser Frage weiterentwickelt hat? Oder haben Sie immer noch modernisierte clevere Anwendungsfälle wie diesen? Und wenn ja, würde es Ihnen etwas ausmachen, ein weiteres interessantes Szenario für uns zu teilen?
Einseitiger

27

Dies wird wie ein bizarrer Vorschlag erscheinen, aber ich habe gelernt, wie man das yieldSchlüsselwort in C # verwendet, indem ich eine Präsentation über Generatoren in Python gelesen habe: David M. Beazleys http://www.dabeaz.com/generators/Generators.pdf . Sie müssen nicht viel Python wissen, um die Präsentation zu verstehen - ich habe es nicht getan. Ich fand es sehr hilfreich, nicht nur zu erklären, wie Generatoren funktionieren, sondern warum Sie sich darum kümmern sollten.


1
Die Präsentation bietet einen einfachen Überblick. Die Details zur Funktionsweise in C # werden von Ray Chen in den Links unter stackoverflow.com/a/39507/939250 erläutert. Der erste Link erklärt ausführlich, dass es am Ende der Renditemethoden eine zweite implizite Rendite gibt.
Donal Lafferty

18

Die Rendite kann für Algorithmen sehr leistungsfähig sein, bei denen Sie Millionen von Objekten durchlaufen müssen. Betrachten Sie das folgende Beispiel, in dem Sie mögliche Fahrten für die Mitfahrgelegenheit berechnen müssen. Zuerst generieren wir mögliche Reisen:

    static IEnumerable<Trip> CreatePossibleTrips()
    {
        for (int i = 0; i < 1000000; i++)
        {
            yield return new Trip
            {
                Id = i.ToString(),
                Driver = new Driver { Id = i.ToString() }
            };
        }
    }

Dann durchlaufen Sie jede Reise:

    static void Main(string[] args)
    {
        foreach (var trip in CreatePossibleTrips())
        {
            // possible trip is actually calculated only at this point, because of yield
            if (IsTripGood(trip))
            {
                // match good trip
            }
        }
    }

Wenn Sie List anstelle von Yield verwenden, müssen Sie 1 Million Objekte dem Speicher zuweisen (~ 190 MB). Die Ausführung dieses einfachen Beispiels dauert ca. 1400 ms. Wenn Sie jedoch Yield verwenden, müssen Sie nicht alle diese temporären Objekte im Speicher ablegen, und Sie erhalten eine erheblich schnellere Algorithmusgeschwindigkeit: In diesem Beispiel dauert die Ausführung ohne Speicherverbrauch nur ca. 400 ms.


2
unter der Decke, was ist Ertrag? Ich hätte gedacht, es wäre eine Liste, also wie würde es die Speichernutzung verbessern?
rollt

1
@rolls yieldarbeitet unter dem Deckmantel, indem eine Zustandsmaschine intern implementiert wird. Hier ist eine SO-Antwort mit 3 detaillierten MSDN-Blog-Posts , die die Implementierung ausführlich erläutern. Geschrieben von Raymond Chen @ MSFT
Shiva

13

Die beiden Codeteile machen wirklich zwei verschiedene Dinge. Die erste Version zieht Mitglieder nach Bedarf. Die zweite Version lädt alle Ergebnisse in den Speicher, bevor Sie damit beginnen.

Es gibt keine richtige oder falsche Antwort auf diese Frage. Welches vorzuziehen ist, hängt nur von der Situation ab. Wenn Sie beispielsweise nur eine begrenzte Zeit zum Abschließen Ihrer Abfrage benötigen und mit den Ergebnissen etwas halbkompliziertes tun müssen, ist die zweite Version möglicherweise vorzuziehen. Achten Sie jedoch auf große Ergebnismengen, insbesondere wenn Sie diesen Code im 32-Bit-Modus ausführen. Ich bin bei dieser Methode mehrmals von OutOfMemory-Ausnahmen gebissen worden.

Das Wichtigste ist jedoch Folgendes: Die Unterschiede liegen in der Effizienz. Daher sollten Sie sich wahrscheinlich für das entscheiden, was Ihren Code einfacher macht, und ihn erst nach der Profilerstellung ändern.


11

Der Ertrag hat zwei großartige Verwendungsmöglichkeiten

Es ist hilfreich, eine benutzerdefinierte Iteration bereitzustellen, ohne temporäre Sammlungen zu erstellen. (Laden aller Daten und Schleifen)

Es hilft, eine zustandsbehaftete Iteration durchzuführen. (Streaming)

Unten ist ein einfaches Video, das ich mit vollständiger Demonstration erstellt habe, um die beiden oben genannten Punkte zu unterstützen

http://www.youtube.com/watch?v=4fju3xcm21M


10

Dies ist, was Chris Sells über diese Aussagen in der C # -Programmiersprache erzählt ;

Ich vergesse manchmal, dass Yield Return nicht dasselbe ist wie Return, da der Code nach einer Yield Return ausgeführt werden kann. Zum Beispiel kann der Code nach der ersten Rückgabe hier niemals ausgeführt werden:

    int F() {
return 1;
return 2; // Can never be executed
}

Im Gegensatz dazu kann der Code nach der ersten Rendite hier ausgeführt werden:

IEnumerable<int> F() {
yield return 1;
yield return 2; // Can be executed
}

Das beißt mich oft in einer if-Anweisung:

IEnumerable<int> F() {
if(...) { yield return 1; } // I mean this to be the only
// thing returned
yield return 2; // Oops!
}

In diesen Fällen ist es hilfreich, sich daran zu erinnern, dass die Rendite nicht wie bei der Rendite „endgültig“ ist.


Um Mehrdeutigkeiten zu vermeiden, klären Sie bitte, wann Sie sagen können, ist das, wird oder könnte? Könnte es möglich sein, dass der erste zurückkehrt und den zweiten Ertrag nicht ausführt?
Johno Crawford

@JohnoCrawford Die zweite Yield-Anweisung wird nur ausgeführt, wenn der zweite / nächste Wert der IEnumerable aufgelistet ist. Es ist durchaus möglich, dass dies nicht der Fall ist, z. B. wird F().Any()dies zurückgegeben, nachdem versucht wurde, nur das erste Ergebnis aufzuzählen. Im Allgemeinen sollten Sie sich nicht darauf verlassen, dass ein IEnumerable yieldProgrammstatus geändert wird, da er möglicherweise nicht ausgelöst wird
Zac Faragher,

8

Angenommen, die LINQ-Klasse Ihrer Produkte verwendet eine ähnliche Ausbeute für die Aufzählung / Iteration. Die erste Version ist effizienter, da sie bei jeder Iteration nur einen Wert liefert.

Das zweite Beispiel ist das Konvertieren des Enumerators / Iterators in eine Liste mit der ToList () -Methode. Dies bedeutet, dass alle Elemente im Enumerator manuell durchlaufen werden und dann eine flache Liste zurückgegeben wird.


8

Dies ist ein bisschen nebensächlich, aber da die Frage als Best Practices gekennzeichnet ist, werde ich meine zwei Cent einwerfen. Für diese Art von Dingen ziehe ich es sehr vor, daraus eine Immobilie zu machen:

public static IEnumerable<Product> AllProducts
{
    get {
        using (AdventureWorksEntities db = new AdventureWorksEntities()) {
            var products = from product in db.Product
                           select product;

            return products;
        }
    }
}

Sicher, es ist etwas mehr Kesselplatte, aber der Code, der dies verwendet, sieht viel sauberer aus:

prices = Whatever.AllProducts.Select (product => product.price);

vs.

prices = Whatever.GetAllProducts().Select (product => product.price);

Hinweis: Ich würde dies nicht für Methoden tun, deren Ausführung eine Weile dauern kann.


7

Und was ist damit?

public static IEnumerable<Product> GetAllProducts()
{
    using (AdventureWorksEntities db = new AdventureWorksEntities())
    {
        var products = from product in db.Product
                       select product;

        return products.ToList();
    }
}

Ich denke, das ist viel sauberer. Ich habe jedoch kein VS2008 zur Hand, um es zu überprüfen. Wenn Products IEnumerable implementiert (wie es scheint - es wird in einer foreach-Anweisung verwendet), würde ich es auf jeden Fall direkt zurückgeben.


2
Bitte bearbeiten Sie OP, um weitere Informationen einzuschließen, anstatt Antworten zu veröffentlichen.
Brian Rasmussen

Nun, du musst mir sagen, wofür OP genau steht :-) Danke
petr k.

Originaler Beitrag, nehme ich an. Ich kann keine Beiträge bearbeiten, daher schien dies der richtige Weg zu sein.
petr k.

5

In diesem Fall hätte ich Version 2 des Codes verwendet. Da Sie über die vollständige Liste der verfügbaren Produkte verfügen und dies vom "Verbraucher" dieses Methodenaufrufs erwartet wird, müssen die vollständigen Informationen an den Anrufer zurückgesendet werden.

Wenn der Aufrufer dieser Methode jeweils "eine" Information benötigt und der Verbrauch der nächsten Information auf Abruf erfolgt, ist es vorteilhaft, die Ertragsrückgabe zu verwenden, um sicherzustellen, dass der Ausführungsbefehl an den Aufrufer zurückgegeben wird, wenn Eine Informationseinheit ist verfügbar.

Einige Beispiele, bei denen man die Rendite verwenden könnte, sind:

  1. Komplexe, schrittweise Berechnung, bei der der Anrufer Schritt für Schritt auf Daten wartet
  2. Paging in der GUI - wo der Benutzer möglicherweise nie die letzte Seite erreicht und nur ein Teil der Informationen auf der aktuellen Seite veröffentlicht werden muss

Um Ihre Fragen zu beantworten, hätte ich die Version 2 verwendet.


3

Geben Sie die Liste direkt zurück. Leistungen:

  • Es ist klarer
  • Die Liste ist wiederverwendbar. (der Iterator ist nicht) nicht wirklich wahr, danke Jon

Sie sollten den Iterator (Yield) verwenden, wenn Sie der Meinung sind, dass Sie wahrscheinlich nicht bis zum Ende der Liste iterieren müssen oder wenn er kein Ende hat. Zum Beispiel wird der Client-Aufruf nach dem ersten Produkt suchen, das ein bestimmtes Prädikat erfüllt. Sie können den Iterator verwenden, obwohl dies ein erfundenes Beispiel ist und es wahrscheinlich bessere Möglichkeiten gibt, dies zu erreichen. Wenn Sie im Voraus wissen, dass die gesamte Liste berechnet werden muss, tun Sie dies einfach im Voraus. Wenn Sie der Meinung sind, dass dies nicht der Fall ist, sollten Sie die Iteratorversion verwenden.


Vergessen Sie nicht, dass es in IEnumerable <T> zurückkehrt, nicht in einem IEnumerator <T> - Sie können GetEnumerator erneut aufrufen.
Jon Skeet

Selbst wenn Sie im Voraus wissen, dass die gesamte Liste berechnet werden muss, kann es dennoch vorteilhaft sein, die Rendite zu verwenden. Ein Beispiel ist, wenn die Sammlung Hunderttausende von Elementen enthält.
Val

1

Die Schlüsselrendite für die Ertragsrückgabe wird verwendet, um die Zustandsmaschine für eine bestimmte Sammlung zu verwalten. Überall dort, wo die CLR die verwendete Schlüsselrendite für die Ertragsrückgabe sieht, implementiert die CLR ein Enumerator-Muster für diesen Code. Diese Art der Implementierung hilft dem Entwickler bei allen Arten von Installationen, die wir ohne das Schlüsselwort sonst hätten ausführen müssen.

Angenommen, der Entwickler filtert eine Sammlung, durchläuft die Sammlung und extrahiert diese Objekte in einer neuen Sammlung. Diese Art der Installation ist ziemlich eintönig.

Mehr zum Schlüsselwort hier in diesem Artikel .


-4

Die Nutzung der Ausbeute ist vergleichbar mit dem Schlüsselwort return , außer dass es einen zurück Generator . Und das Generatorobjekt wird nur einmal durchlaufen .

Ertrag hat zwei Vorteile:

  1. Sie müssen diese Werte nicht zweimal lesen.
  2. Sie können viele untergeordnete Knoten abrufen, müssen jedoch nicht alle im Speicher ablegen.

Es gibt eine andere klare Erklärung , die Ihnen vielleicht helfen könnte.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.