Frage

Im Wie kann ich nur ein Fragment von IList<> verfügbar machen? Frage eins der Antworten enthielt den folgenden Codeausschnitt:

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

Was macht das Yield-Keyword dort?Ich habe an einigen Stellen Hinweise darauf gesehen und eine andere Frage gestellt, aber ich habe nicht ganz herausgefunden, was es tatsächlich bewirkt.Ich bin es gewohnt, unter Yield in dem Sinne zu denken, dass ein Thread einem anderen nachgibt, aber das scheint hier nicht relevant zu sein.

War es hilfreich?

Lösung

Der yield Das Schlüsselwort macht hier tatsächlich ziemlich viel.

Die Funktion gibt ein Objekt zurück, das das implementiert IEnumerable<object> Schnittstelle.Wenn eine aufrufende Funktion startet foreachBeim Überfahren dieses Objekts wird die Funktion erneut aufgerufen, bis sie „nachgibt“.Dies ist syntaktischer Zucker, der eingeführt wurde C# 2.0.In früheren Versionen mussten Sie Ihre eigenen erstellen IEnumerable Und IEnumerator Objekte, um solche Dinge zu tun.

Der einfachste Weg, solchen Code zu verstehen, besteht darin, ein Beispiel einzugeben, einige Haltepunkte zu setzen und zu sehen, was passiert.Versuchen Sie, dieses Beispiel durchzugehen:

public void Consumer()
{
    foreach(int i in Integers())
    {
        Console.WriteLine(i.ToString());
    }
}

public IEnumerable<int> Integers()
{
    yield return 1;
    yield return 2;
    yield return 4;
    yield return 8;
    yield return 16;
    yield return 16777216;
}

Wenn Sie das Beispiel durchgehen, finden Sie den ersten Aufruf von Integers() kehrt zurück 1.Der zweite Anruf kehrt zurück 2 und die Linie yield return 1 wird nicht erneut ausgeführt.

Hier ist ein Beispiel aus der Praxis:

public IEnumerable<T> Read<T>(string sql, Func<IDataReader, T> make, params object[] parms)
{
    using (var connection = CreateConnection())
    {
        using (var command = CreateCommand(CommandType.Text, sql, connection, parms))
        {
            command.CommandTimeout = dataBaseSettings.ReadCommandTimeout;
            using (var reader = command.ExecuteReader())
            {
                while (reader.Read())
                {
                    yield return make(reader);
                }
            }
        }
    }
}

Andere Tipps

Wiederholung.Es erstellt „unter der Decke“ eine Zustandsmaschine, die sich bei jedem weiteren Zyklus der Funktion merkt, wo Sie sich befanden, und von dort aus weitermacht.

Ertrag hat zwei großartige Verwendungsmöglichkeiten:

  1. Es hilft, benutzerdefinierte Iterationen bereitzustellen, ohne temporäre Sammlungen zu erstellen.

  2. Es hilft, eine zustandsbehaftete Iteration durchzuführen.enter image description here

Um die beiden oben genannten Punkte anschaulicher zu erklären, habe ich ein einfaches Video erstellt, das Sie sich ansehen können Hier

Kürzlich veröffentlichte Raymond Chen auch eine interessante Artikelserie zum Schlüsselwort „Yield“.

Obwohl es nominell zur einfachen Implementierung eines Iteratormusters verwendet wird, kann es in eine Zustandsmaschine verallgemeinert werden.Es hat keinen Sinn, Raymond zu zitieren, der letzte Teil verweist auch auf andere Verwendungszwecke (aber das Beispiel in Entins Blog ist besonders gut und zeigt, wie man asynchronen sicheren Code schreibt).

Auf den ersten Blick ist yield return ein .NET-Zucker, um ein IEnumerable zurückzugeben.

Ohne yield werden alle Elemente der Sammlung auf einmal erstellt:

class SomeData
{
    public SomeData() { }

    static public IEnumerable<SomeData> CreateSomeDatas()
    {
        return new List<SomeData> {
            new SomeData(), 
            new SomeData(), 
            new SomeData()
        };
    }
}

Derselbe Code verwendet yield, es wird Element für Element zurückgegeben:

class SomeData
{
    public SomeData() { }

    static public IEnumerable<SomeData> CreateSomeDatas()
    {
        yield return new SomeData();
        yield return new SomeData();
        yield return new SomeData();
    }
}

Der Vorteil der Verwendung von yield besteht darin, dass die restlichen Elemente nicht erstellt werden, wenn die Funktion, die Ihre Daten verbraucht, lediglich das erste Element der Sammlung benötigt.

Der Yield-Operator ermöglicht die Erstellung von Artikeln nach Bedarf.Das ist ein guter Grund, es zu nutzen.

yield return wird mit Enumeratoren verwendet.Bei jedem Aufruf der yield-Anweisung wird die Kontrolle an den Aufrufer zurückgegeben, es wird jedoch sichergestellt, dass der Status des Angerufenen beibehalten wird.Aus diesem Grund setzt der Aufrufer beim Aufzählen des nächsten Elements die Ausführung in der aufgerufenen Methode von der Anweisung unmittelbar nach dem fort yield Stellungnahme.

Versuchen wir dies anhand eines Beispiels zu verstehen.In diesem Beispiel habe ich für jede Zeile die Reihenfolge angegeben, in der die Ausführung erfolgt.

static void Main(string[] args)
{
    foreach (int fib in Fibs(6))//1, 5
    {
        Console.WriteLine(fib + " ");//4, 10
    }            
}

static IEnumerable<int> Fibs(int fibCount)
{
    for (int i = 0, prevFib = 0, currFib = 1; i < fibCount; i++)//2
    {
        yield return prevFib;//3, 9
        int newFib = prevFib + currFib;//6
        prevFib = currFib;//7
        currFib = newFib;//8
    }
}

Außerdem wird der Status für jede Aufzählung beibehalten.Angenommen, ich habe einen weiteren Anruf Fibs() Methode dann wird der Status dafür zurückgesetzt.

Intuitiv gibt das Schlüsselwort einen Wert von der Funktion zurück, ohne diese zu verlassen, d. h.In Ihrem Codebeispiel wird der Strom zurückgegeben item Wert und setzt dann die Schleife fort.Formaler ausgedrückt wird es vom Compiler verwendet, um Code für eine zu generieren Iterator.Iteratoren sind Funktionen, die zurückkehren IEnumerable Objekte.Der MSDN hat verschiedene Artikel über sie.

Eine Listen- oder Array-Implementierung lädt alle Elemente sofort, während die Yield-Implementierung eine Lösung mit verzögerter Ausführung bietet.

In der Praxis ist es häufig wünschenswert, den erforderlichen Arbeitsaufwand so gering wie möglich zu halten, um den Ressourcenverbrauch einer Anwendung zu reduzieren.

Beispielsweise verfügen wir möglicherweise über eine Anwendung, die Millionen von Datensätzen aus einer Datenbank verarbeitet.Die folgenden Vorteile können erzielt werden, wenn wir IEnumerable in einem Pull-basierten Modell mit verzögerter Ausführung verwenden:

  • Skalierbarkeit, Zuverlässigkeit und Vorhersagbarkeit werden sich wahrscheinlich verbessern, da die Anzahl der Datensätze keinen wesentlichen Einfluss auf die Ressourcenanforderungen der Anwendung hat.
  • Leistung und Reaktionsfähigkeit werden sich wahrscheinlich verbessern, da die Verarbeitung sofort beginnen kann, anstatt darauf zu warten, dass die gesamte Sammlung zuerst geladen wird.
  • Wiederherstellbarkeit und Nutzung werden sich wahrscheinlich verbessern, da die Anwendung gestoppt, gestartet, unterbrochen oder fehlschlagen kann.Im Vergleich zum Vorababruf aller Daten, bei dem nur ein Teil der Ergebnisse tatsächlich verwendet wurde, gehen nur die in Bearbeitung befindlichen Elemente verloren.
  • Kontinuierliche Verarbeitung ist in Umgebungen möglich, in denen konstante Arbeitslastströme hinzugefügt werden.

Hier ist ein Vergleich zwischen dem Erstellen einer Sammlung, z. B. einer Liste, und der Verwendung von yield.

Listenbeispiel

    public class ContactListStore : IStore<ContactModel>
    {
        public IEnumerable<ContactModel> GetEnumerator()
        {
            var contacts = new List<ContactModel>();
            Console.WriteLine("ContactListStore: Creating contact 1");
            contacts.Add(new ContactModel() { FirstName = "Bob", LastName = "Blue" });
            Console.WriteLine("ContactListStore: Creating contact 2");
            contacts.Add(new ContactModel() { FirstName = "Jim", LastName = "Green" });
            Console.WriteLine("ContactListStore: Creating contact 3");
            contacts.Add(new ContactModel() { FirstName = "Susan", LastName = "Orange" });
            return contacts;
        }
    }

    static void Main(string[] args)
    {
        var store = new ContactListStore();
        var contacts = store.GetEnumerator();

        Console.WriteLine("Ready to iterate through the collection.");
        Console.ReadLine();
    }

Konsolenausgabe
ContactListStore:Kontakt erstellen 1
ContactListStore:Kontakt erstellen 2
ContactListStore:Kontakt erstellen 3
Bereit, die Sammlung zu durchlaufen.

Notiz:Die gesamte Sammlung wurde in den Speicher geladen, ohne dass auch nur ein einziges Element in der Liste abgefragt wurde

Ertragsbeispiel

public class ContactYieldStore : IStore<ContactModel>
{
    public IEnumerable<ContactModel> GetEnumerator()
    {
        Console.WriteLine("ContactYieldStore: Creating contact 1");
        yield return new ContactModel() { FirstName = "Bob", LastName = "Blue" };
        Console.WriteLine("ContactYieldStore: Creating contact 2");
        yield return new ContactModel() { FirstName = "Jim", LastName = "Green" };
        Console.WriteLine("ContactYieldStore: Creating contact 3");
        yield return new ContactModel() { FirstName = "Susan", LastName = "Orange" };
    }
}

static void Main(string[] args)
{
    var store = new ContactYieldStore();
    var contacts = store.GetEnumerator();

    Console.WriteLine("Ready to iterate through the collection.");
    Console.ReadLine();
}

Konsolenausgabe
Bereit, die Sammlung zu durchlaufen.

Notiz:Die Sammlung wurde überhaupt nicht durchgeführt.Dies liegt an der Art der „verzögerten Ausführung“ von IEnumerable.Der Bau eines Artikels erfolgt nur dann, wenn er wirklich benötigt wird.

Rufen wir die Sammlung erneut auf und beobachten das Verhalten, wenn wir den ersten Kontakt in der Sammlung abrufen.

static void Main(string[] args)
{
    var store = new ContactYieldStore();
    var contacts = store.GetEnumerator();
    Console.WriteLine("Ready to iterate through the collection");
    Console.WriteLine("Hello {0}", contacts.First().FirstName);
    Console.ReadLine();
}

Konsolenausgabe
Bereit, die Sammlung zu durchlaufen
KontaktYieldStore:Kontakt erstellen 1
Hallo Bob

Hübsch!Erst der erste Kontakt kam zustande, als der Kunde das Objekt aus der Sammlung „herauszog“.

Hier ist eine einfache Möglichkeit, das Konzept zu verstehen:Die Grundidee ist: Wenn Sie eine Sammlung wünschen, die Sie verwenden können,foreach" an, aber das Sammeln der Elemente in der Sammlung ist aus irgendeinem Grund teuer (z. B. das Abfragen aus einer Datenbank), UND Sie benötigen häufig nicht die gesamte Sammlung. Dann erstellen Sie eine Funktion, die die Sammlung Element für Element erstellt, und gibt es an den Verbraucher zurück (der dann die Sammelbemühungen vorzeitig beenden kann).

Denk darüber so: Sie gehen zur Fleischtheke und möchten ein Pfund Schinkenscheiben kaufen.Der Metzger nimmt einen 10-Pfund-Schinken mit nach hinten, legt ihn auf die Schneidemaschine, schneidet das Ganze in Scheiben, bringt dann den Stapel Scheiben zu Ihnen zurück und misst ein Pfund davon ab.(Alter Weg).Mit yield, bringt der Metzger die Aufschnittmaschine zur Theke und beginnt mit dem Schneiden und „Ausgeben“ jeder Scheibe auf die Waage, bis sie 1 Pfund misst, dann verpackt sie sie für Sie und fertig. Der alte Weg mag für den Metzger besser sein (er kann seine Maschinen so organisieren, wie er möchte), aber der neue Weg ist in den meisten Fällen für den Verbraucher deutlich effizienter.

Der yield Mit dem Schlüsselwort können Sie eine erstellen IEnumerable<T> in der Form auf einem Iteratorblock.Dieser Iteratorblock unterstützt verzögerte Ausführung und wenn Sie mit dem Konzept nicht vertraut sind, kann es fast magisch erscheinen.Letztlich handelt es sich jedoch nur um Code, der ohne seltsame Tricks ausgeführt wird.

Ein Iteratorblock kann als syntaktischer Zucker beschrieben werden, bei dem der Compiler eine Zustandsmaschine generiert, die verfolgt, wie weit die Aufzählung des Aufzählbaren fortgeschritten ist.Um eine Aufzählung aufzuzählen, verwenden Sie häufig a foreach Schleife.Allerdings a foreach Schleife ist auch syntaktischer Zucker.Sie sind also zwei vom echten Code entfernte Abstraktionen, weshalb es zunächst schwierig sein könnte, zu verstehen, wie alles zusammenwirkt.

Gehen Sie davon aus, dass Sie einen sehr einfachen Iteratorblock haben:

IEnumerable<int> IteratorBlock()
{
    Console.WriteLine("Begin");
    yield return 1;
    Console.WriteLine("After 1");
    yield return 2;
    Console.WriteLine("After 2");
    yield return 42;
    Console.WriteLine("End");
}

Echte Iteratorblöcke haben oft Bedingungen und Schleifen, aber wenn Sie die Bedingungen überprüfen und die Schleifen abwickeln, werden sie immer noch so angezeigt yield Anweisungen, die mit anderem Code verschachtelt sind.

Um den Iteratorblock aufzuzählen a foreach Schleife wird verwendet:

foreach (var i in IteratorBlock())
    Console.WriteLine(i);

Hier ist die Ausgabe (hier gibt es keine Überraschungen):

Begin
1
After 1
2
After 2
42
End

Wie oben erwähnt foreach ist syntaktischer Zucker:

IEnumerator<int> enumerator = null;
try
{
    enumerator = IteratorBlock().GetEnumerator();
    while (enumerator.MoveNext())
    {
        var i = enumerator.Current;
        Console.WriteLine(i);
    }
}
finally
{
    enumerator?.Dispose();
}

Um dies zu entwirren, habe ich ein Sequenzdiagramm erstellt, in dem die Abstraktionen entfernt wurden:

C# iterator block sequence diagram

Die vom Compiler generierte Zustandsmaschine implementiert auch den Enumerator, aber um das Diagramm klarer zu machen, habe ich sie als separate Instanzen dargestellt.(Wenn die Zustandsmaschine von einem anderen Thread aus aufgezählt wird, erhalten Sie tatsächlich separate Instanzen, aber dieses Detail ist hier nicht wichtig.)

Jedes Mal, wenn Sie Ihren Iteratorblock aufrufen, wird eine neue Instanz der Zustandsmaschine erstellt.Allerdings wird bis dahin keiner Ihrer Codes im Iteratorblock ausgeführt enumerator.MoveNext() wird zum ersten Mal ausgeführt.So funktioniert die verzögerte Ausführung.Hier ist ein (ziemlich albernes) Beispiel:

var evenNumbers = IteratorBlock().Where(i => i%2 == 0);

Zu diesem Zeitpunkt wurde der Iterator noch nicht ausgeführt.Der Where Klausel erstellt eine neue IEnumerable<T> das umhüllt die IEnumerable<T> zurückgegeben von IteratorBlock aber diese Aufzählung muss noch aufgezählt werden.Dies geschieht, wenn Sie a ausführen foreach Schleife:

foreach (var evenNumber in evenNumbers)
    Console.WriteLine(eventNumber);

Wenn Sie die Aufzählung zweimal aufzählen, wird jedes Mal eine neue Instanz der Zustandsmaschine erstellt und Ihr Iteratorblock führt denselben Code zweimal aus.

Beachten Sie, dass LINQ-Methoden wie ToList(), ToArray(), First(), Count() usw.werde a verwenden foreach Schleife zum Aufzählen des Aufzählbaren.Zum Beispiel ToList() listet alle Elemente der Aufzählung auf und speichert sie in einer Liste.Sie können nun auf die Liste zugreifen, um alle Elemente der Aufzählung abzurufen, ohne dass der Iteratorblock erneut ausgeführt wird.Es besteht ein Kompromiss zwischen der Verwendung der CPU zum mehrfachen Erzeugen der Elemente der Aufzählung und dem Speicher zum Speichern der Elemente der Aufzählung, um mehrmals darauf zugreifen zu können, wenn Methoden wie verwendet werden ToList().

Wenn ich das richtig verstehe, würde ich es aus der Perspektive der Funktion, die IEnumerable mit yield implementiert, folgendermaßen formulieren.

  • Hier ist eine.
  • Rufen Sie noch einmal an, wenn Sie einen weiteren benötigen.
  • Ich werde mich daran erinnern, was ich dir bereits gegeben habe.
  • Ich werde erst wissen, ob ich Ihnen ein anderes geben kann, wenn Sie erneut anrufen.

Das C#-Schlüsselwort yield erlaubt, um es einfach auszudrücken, viele Aufrufe eines Codekörpers, der als Iterator bezeichnet wird, der weiß, wie er zurückkehren muss, bevor er fertig ist, und der bei einem erneuten Aufruf dort fortfährt, wo er aufgehört hat – d. h.Es hilft einem Iterator, für jedes Element in einer Sequenz, die der Iterator in aufeinanderfolgenden Aufrufen zurückgibt, transparent zustandsbehaftet zu werden.

In JavaScript wird das gleiche Konzept als Generatoren bezeichnet.

Es ist eine sehr einfache und unkomplizierte Möglichkeit, eine Aufzählung für Ihr Objekt zu erstellen.Der Compiler erstellt eine Klasse, die Ihre Methode umschließt und in diesem Fall IEnumerable<object> implementiert.Ohne das Schlüsselwort yield müssten Sie ein Objekt erstellen, das IEnumerable<object> implementiert.

Es erzeugt eine aufzählbare Sequenz.Tatsächlich wird eine lokale IEnumerable-Sequenz erstellt und als Methodenergebnis zurückgegeben

Das Verknüpfung hat ein einfaches Beispiel

Noch einfachere Beispiele finden Sie hier

public static IEnumerable<int> testYieldb()
{
    for(int i=0;i<3;i++) yield return 4;
}

Beachten Sie, dass yield return von der Methode nicht zurückgegeben wird.Sie können sogar eine setzen WriteLine nach dem yield return

Das Obige erzeugt ein IEnumerable mit 4 Ints 4,4,4,4

Hier mit einem WriteLine.Fügt 4 zur Liste hinzu, gibt abc aus, fügt dann 4 zur Liste hinzu, schließt dann die Methode ab und kehrt so wirklich von der Methode zurück (sobald die Methode abgeschlossen ist, wie es bei einer Prozedur ohne Rückgabe der Fall wäre).Aber das hätte einen Wert, einen IEnumerable Liste von ints, dass es nach Abschluss zurückkehrt.

public static IEnumerable<int> testYieldb()
{
    yield return 4;
    console.WriteLine("abc");
    yield return 4;
}

Beachten Sie auch, dass bei Verwendung von yield das, was Sie zurückgeben, nicht vom gleichen Typ ist wie die Funktion.Es ist vom Typ eines Elements innerhalb der IEnumerable Liste.

Sie verwenden yield mit dem Rückgabetyp der Methode as IEnumerable.Wenn der Rückgabetyp der Methode ist int oder List<int> und du verwendest yield, dann wird es nicht kompiliert.Sie können verwenden IEnumerable Methodenrückgabetyp ohne Yield, aber es scheint, dass Sie Yield vielleicht nicht ohne verwenden können IEnumerable Methodenrückgabetyp.

Und damit es ausgeführt wird, muss man es auf eine besondere Art und Weise aufrufen.

static void Main(string[] args)
{
    testA();
    Console.Write("try again. the above won't execute any of the function!\n");

    foreach (var x in testA()) { }


    Console.ReadLine();
}



// static List<int> testA()
static IEnumerable<int> testA()
{
    Console.WriteLine("asdfa");
    yield return 1;
    Console.WriteLine("asdf");
}

Es versucht etwas Ruby Goodness hereinzubringen :)
Konzept: Dies ist ein Beispiel für Ruby-Code, der jedes Element des Arrays ausgibt

 rubyArray = [1,2,3,4,5,6,7,8,9,10]
    rubyArray.each{|x| 
        puts x   # do whatever with x
    }

Die Implementierung jeder Methode des Arrays Erträge Kontrolle über den Anrufer (die 'Puts x') mit jede Element des Arrays, übersichtlich dargestellt als x.Der Aufrufer kann dann mit x alles tun, was er tun muss.

Jedoch .Netz geht hier nicht bis zum Ende..C# scheint yield mit IEnumerable gekoppelt zu haben, was Sie in gewisser Weise dazu zwingt, eine foreach-Schleife im Aufrufer zu schreiben, wie in Mendelts Antwort zu sehen ist.Etwas weniger elegant.

//calling code
foreach(int i in obCustomClass.Each())
{
    Console.WriteLine(i.ToString());
}

// CustomClass implementation
private int[] data = {1,2,3,4,5,6,7,8,9,10};
public IEnumerable<int> Each()
{
   for(int iLooper=0; iLooper<data.Length; ++iLooper)
        yield return data[iLooper]; 
}
Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top