Eine praktische Verwendung des Schlüsselworts "yield" in C # [closed]

76

Nach fast 4 Jahren Erfahrung habe ich keinen Code gesehen, in dem das Schlüsselwort yield verwendet wird. Kann mir jemand eine praktische Verwendung (zusammen mit einer Erklärung) dieses Schlüsselworts zeigen, und wenn ja, gibt es keine anderen Möglichkeiten, die Erfüllung der Aufgaben zu erleichtern?

Saeed Neamati
quelle
9
Alle (oder zumindest die meisten) LINQ-Komponenten werden mit Yield implementiert. Auch das Unity3D-Framework hat eine gute Verwendung gefunden - es wird verwendet, um Funktionen (bei Ertragsaussagen) anzuhalten und später mit dem Status in der IEnumerable fortzusetzen.
Dani
2
Sollte dies nicht auf StackOverflow verschoben werden?
Danny Varod
4
@Danny - Es ist nicht für Stack Overflow geeignet, da die Frage nicht nach der Lösung eines bestimmten Problems, sondern yieldnach den allgemeinen Verwendungsmöglichkeiten lautet .
ChrisF
9
Wirklich? Ich kann mir keine einzige App vorstellen, in der ich sie nicht verwendet habe.
Aaronaught

Antworten:

107

Effizienz

Das yieldSchlüsselwort erstellt effektiv eine verzögerte Aufzählung von Sammlungselementen, die viel effizienter sein kann. Wenn Ihre foreachSchleife beispielsweise nur die ersten 5 Elemente von 1 Million Elementen durchläuft, ist dies alles ein yieldErgebnis, und Sie haben nicht zuerst intern eine Sammlung von 1 Million Elementen erstellt. Ebenso wollen Sie verwenden yieldmit IEnumerable<T>Rückgabewerte in der eigenen Programmierung Szenarien die gleichen Wirkungsgrade zu erreichen.

Beispiel für die in einem bestimmten Szenario erzielte Effizienz

Keine Iteratormethode, potenziell ineffiziente Verwendung einer großen Sammlung.
(Die Zwischensammlung besteht aus vielen Elementen.)

// Method returns all million items before anything can loop over them. 
List<object> GetAllItems() {
    List<object> millionCustomers;
    database.LoadMillionCustomerRecords(millionCustomers); 
    return millionCustomers;
}

// MAIN example ---------------------
// Caller code sample:
int num = 0;
foreach(var itm in GetAllItems())  {
    num++;
    if (num == 5)
        break;
}
// Note: One million items returned, but only 5 used. 

Iterator-Version, effizient
(Es wird keine Zwischensammlung erstellt)

// Yields items one at a time as the caller's foreach loop requests them
IEnumerable<object> IterateOverItems() {
    for (int i; i < database.Customers.Count(); ++i)
        yield return database.Customers[i];
}

// MAIN example ---------------------
// Caller code sample:
int num = 0;
foreach(var itm in IterateOverItems())  {
    num++;
    if (num == 5)
        break;
}
// Note: Only 5 items were yielded and used out of the million.

Vereinfachen Sie einige Programmierszenarien

In einem anderen Fall werden einige Arten des Sortierens und Zusammenführens von Listen einfacher programmiert, da Sie die yieldElemente nur in der gewünschten Reihenfolge zurücksortieren, anstatt sie in eine Zwischensammlung zu sortieren und dort einzutauschen. Es gibt viele solcher Szenarien.

Nur ein Beispiel ist das Zusammenführen von zwei Listen:

IEnumerable<object> EfficientMerge(List<object> list1, List<object> list2) {
    foreach(var o in list1) 
        yield return o; 
    foreach(var o in list2) 
        yield return o;
}

Mit dieser Methode wird eine zusammenhängende Liste von Elementen zurückgegeben, dh eine Zusammenführung, für die keine Zwischensammlung erforderlich ist.

Mehr Info

Das yieldSchlüsselwort kann nur im Zusammenhang mit einem Iterator - Verfahren verwendet werden (einen Rückgabetyp aufweisen IEnumerable, IEnumerator, IEnumerable<T>, oder IEnumerator<T>.) , Und es ist eine besondere Beziehung foreach. Iteratoren sind spezielle Methoden. Die MSDN-Ertrags- und Iteratordokumentation enthält viele interessante Informationen und Erläuterungen zu den Konzepten. Korrelieren Sie es unbedingt mit dem foreachSchlüsselwort, indem Sie es auch lesen, um Ihr Verständnis von Iteratoren zu ergänzen.

Um zu erfahren, wie die Iteratoren ihre Effizienz erreichen, befindet sich das Geheimnis im vom C # -Compiler generierten IL-Code. Die für eine Iteratormethode generierte IL unterscheidet sich drastisch von der für eine reguläre (Nicht-Iterator-) Methode generierten. Dieser Artikel (Was generiert das Yield-Schlüsselwort wirklich?) Bietet diese Art von Einsicht.

John K
quelle
2
Sie sind besonders nützlich für Algorithmen, die eine (möglicherweise lange) Sequenz benötigen und eine andere generieren, bei der die Zuordnung nicht eins zu eins ist. Ein Beispiel hierfür ist das Ausschneiden von Polygonen. Jede bestimmte Kante kann viele oder gar keine Kanten erzeugen, sobald sie abgeschnitten sind. Iteratoren machen es enorm einfacher, dies auszudrücken, und Nachgeben ist eine der besten Möglichkeiten, sie zu schreiben.
Donal Fellows
+1 Viel bessere Antwort, als ich aufgeschrieben hatte. Jetzt habe ich auch gelernt, dass Ertrag für bessere Leistung gut ist.
Jan_V
3
Es war einmal, als ich mit yield Pakete für ein binäres Netzwerkprotokoll erstellt habe. Es schien die natürlichste Wahl in C #.
György Andrasek
4
Zählt das nicht die database.Customers.Count()gesamte Aufzählung der Kunden auf, so dass der effizientere Code erforderlich ist, um alle Elemente durchzugehen?
Stephen
5
Nenn mich anal, aber das ist Verkettung, keine Verschmelzung. (Und Linq hat bereits eine Concat-Methode.)
OldFart
4

Vor einiger Zeit hatte ich ein praktisches Beispiel. Nehmen wir an, Sie haben eine Situation wie diese:

List<Button> buttons = new List<Button>();
void AddButtons()
{
   for ( int i = 0; i <= 10; i++ ) {
      var button = new Button();
      buttons.Add(button);
      button.Click += (sender, e) => 
          MessageBox.Show(String.Format("You clicked button number {0}", ???));
   }
}

Das Schaltflächenobjekt kennt seine eigene Position in der Sammlung nicht. Die gleiche Einschränkung gilt für Dictionary<T>oder andere Sammlungsarten.

Hier ist meine Lösung mit yieldSchlüsselwort:

interface IHasId { int Id { get; set; } }

class IndexerList<T>: List<T>, IEnumerable<T> where T: IHasId
{
   List<T> elements = new List<T>();
   new public void Clear() { elements.Clear(); }
   new public void Add(T element) { elements.Add(element); }
   new public int Count { get { return elements.Count; } }    
   new public IEnumerator<T> GetEnumerator()
   {
      foreach ( T c in elements )
         yield return c;
   }

   new public T this[int index]
   {
      get
      {
         foreach ( T c in elements ) {
            if ( (int)c.Id == index )
               return c;
         }
         return default(T);
      }
   }
}

Und so benutze ich es:

class ButtonWithId: Button, IHasId
{
   public int Id { get; private set; }
   public ButtonWithId(int id) { this.Id = id; }
}

IndexerList<ButtonWithId> buttons = new IndexerList<ButtonWithId>();
void AddButtons()
{
   for ( int i = 10; i <= 20; i++ ) {
      var button = new ButtonWithId(i);
      buttons.Add(button);
      button.Click += (sender, e) => 
         MessageBox.Show(String.Format("You clicked button number {0}", ( (ButtonWithId)sender ).Id));
   }
}

Ich muss formeine Sammlung nicht durchlaufen, um den Index zu finden. Mein Button hat eine ID und diese wird auch als Index verwendet IndexerList<T>, so dass Sie redundante IDs oder Indizes vermeiden - das gefällt mir! Der Index / die ID kann eine beliebige Zahl sein.

Wernfried Domscheit
quelle
2

Ein praktisches Beispiel finden Sie hier:

http://www.ytechie.com/2009/02/using-c-yield-for-readability-and-performance.html

Die Verwendung von yield bietet gegenüber Standardcode eine Reihe von Vorteilen:

  • Wenn der Iterator zum Erstellen einer Liste verwendet wird, können Sie die Rendite abrufen und der Aufrufer kann entscheiden, ob er das Ergebnis in einer Liste haben möchte oder nicht.
  • Der Anrufer kann auch entscheiden, die Iteration aus einem Grund abzubrechen, der außerhalb des Bereichs liegt, den Sie in der Iteration ausführen.
  • Code ist etwas kürzer.

Allerdings, wie Jan_V sagte (schlagen Sie mich nur um ein paar Sekunden :-) Sie können ohne es leben, weil der Compiler intern Code in beiden Fällen fast identisch produzieren wird.

Jalayn
quelle
1

Hier ist ein Beispiel:

https://bitbucket.org/ant512/workingweek/src/a745d02ba16f/source/WorkingWeek/Week.cs#cl-158

Die Klasse führt Datumsberechnungen basierend auf einer Arbeitswoche durch. Ich kann einer Instanz der Klasse sagen, dass Bob jeden Tag von 9:30 bis 17:30 Uhr mit einer Mittagspause von 12:30 Uhr arbeitet. Mit diesem Wissen liefert die Funktion AscendingShifts () Arbeitsschichtobjekte zwischen den angegebenen Daten. Um alle Arbeitsschichten von Bob zwischen dem 1. Januar und dem 1. Februar dieses Jahres aufzulisten, verwenden Sie sie wie folgt:

foreach (var shift in week.AscendingShifts(new DateTime(2011, 1, 1), new DateTime(2011, 2, 1)) {
    Console.WriteLine(shift);
}

Die Klasse durchläuft eine Sammlung nicht wirklich. Die Verschiebungen zwischen zwei Daten können jedoch als Sammlung betrachtet werden. Der yieldOperator ermöglicht das Durchlaufen dieser imaginären Sammlung, ohne die Sammlung selbst zu erstellen.

Ameise
quelle
1

Ich habe eine kleine DB-Datenschicht, die eine commandKlasse hat, in der Sie den SQL-Befehlstext, den Befehlstyp und eine IEnumerable von "Befehlsparametern" zurückgeben.

Grundsätzlich besteht die Idee darin, CLR-Befehle einzugeben, anstatt SqlCommandEigenschaften und Parameter ständig manuell auszufüllen .

Es gibt also eine Funktion, die so aussieht:

IEnumerable<DbParameter> GetParameters()
{
    // here i do something like

    yield return new DbParameter { name = "@Age", value = this.Age };

    yield return new DbParameter { name = "@Name", value = this.Name };
}

Die Klasse, die diese commandKlasse erbt , hat die Eigenschaften Ageund Name.

Dann können Sie ein commandObjekt neu anlegen, dessen Eigenschaften gefüllt sind, und es an eine dbSchnittstelle übergeben, die den Befehlsaufruf tatsächlich ausführt.

Alles in allem ist es wirklich einfach, mit SQL-Befehlen zu arbeiten und sie zu tippen.

John
quelle
1

Obwohl der Zusammenführungsfall bereits in der akzeptierten Antwort behandelt wurde, zeige ich Ihnen die Erweiterungsmethode für Yield-Merge-Parameter ™:

public static IEnumerable<T> AppendParams<T>(this IEnumerable<T> a, params T[] b)
{
    foreach (var el in a) yield return el;
    foreach (var el in b) yield return el;
}

Ich benutze dies, um Pakete eines Netzwerkprotokolls zu erstellen:

static byte[] MakeCommandPacket(string cmd)
{
    return
        header
        .AppendParams<byte>(0, 0, 1, 0, 0, 1, 0x92, 0, 0, 0, 0)
        .AppendAscii(cmd)
        .MarkLength()
        .MarkChecksum()
        .ToArray();
}

Die MarkChecksumMethode sieht zum Beispiel so aus. Und es hat auch eine yield:

public static IEnumerable<byte> MarkChecksum(this IEnumerable<byte> data, int pos = 6)
{
    foreach (byte b in data)
    {
        yield return pos-- == 0 ? (byte)data.Sum(z => z) : b;
    }
}

Seien Sie jedoch vorsichtig, wenn Sie Aggregatmethoden wie Sum () in einer Aufzählungsmethode verwenden, da diese einen separaten Aufzählungsprozess auslösen.

Jegor
quelle
1

Elastic Search .NET-Beispiel-Repository bietet ein hervorragendes Beispiel für die yield returnPartitionierung einer Sammlung in mehrere Sammlungen mit einer bestimmten Größe:

https://github.com/elastic/elasticsearch-net-example/blob/master/src/NuSearch.Domain/Extensions/PartitionExtension.cs

public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> source, int size)
    {
        T[] array = null;
        int count = 0;
        foreach (T item in source)
        {
            if (array == null)
            {
                array = new T[size];
            }
            array[count] = item;
            count++;
            if (count == size)
            {
                yield return new ReadOnlyCollection<T>(array);
                array = null;
                count = 0;
            }
        }
        if (array != null)
        {
            Array.Resize(ref array, count);
            yield return new ReadOnlyCollection<T>(array);
        }
    }
pholly
quelle
0

Um auf die Antwort von Jan_V einzugehen, bin ich auf einen konkreten Fall gestoßen, der damit zusammenhängt:

Ich musste die Kernel32-Versionen von FindFirstFile / FindNextFile verwenden. Sie erhalten ein Handle vom ersten Aufruf und geben es an alle nachfolgenden Aufrufe weiter. Wenn Sie dies in einen Enumerator packen, erhalten Sie etwas, das Sie direkt mit foreach verwenden können.

Loren Pechtel
quelle