it-swarm.dev

Eine praktische Verwendung des Schlüsselworts "Yield" in C #

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

77
Saeed Neamati

Effizienz

Das Schlüsselwort yield erstellt effektiv eine verzögerte Aufzählung von Sammlungselementen, die viel effizienter sein kann. Wenn Ihre foreach -Schleife beispielsweise nur die ersten 5 Elemente von 1 Million Elementen durchläuft, wird nur yield zurückgegeben, und Sie haben nicht zuerst intern eine Sammlung von 1 Million Elementen erstellt. Ebenso sollten Sie yield mit IEnumerable<T> Rückgabewerten in Ihren eigenen Programmierszenarien verwenden, um die gleichen Effizienzvorteile zu erzielen.

Beispiel für Effizienz in einem bestimmten Szenario

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 ist es einfacher, einige Arten des Sortierens und Zusammenführens von Listen zu programmieren, da Sie nur yield Elemente in der gewünschten Reihenfolge zurücksetzen, anstatt sie in eine Zwischensammlung zu sortieren und dort auszutauschen. 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;
}

Diese Methode liefert eine zusammenhängende Liste von Elementen zurück, effektiv eine Zusammenführung, ohne dass eine Zwischensammlung erforderlich ist.

Mehr Info

Das Schlüsselwort yield kann nur im Kontext einer Iteratormethode verwendet werden (mit dem Rückgabetyp IEnumerable, IEnumerator, IEnumerable<T> Oder IEnumerator<T>.) und es gibt eine besondere Beziehung zu foreach. Iteratoren sind spezielle Methoden. Die MSDN Yield Documentation und Iterator Documentation enthalten viele interessante Informationen und Erläuterungen zu den Konzepten. Stellen Sie sicher, dass Sie es mit dem Schlüsselwort foreach korrelieren, indem Sie auch darüber lesen, um Ihr Verständnis von Iteratoren zu ergänzen.

Um zu erfahren, wie die Iteratoren ihre Effizienz erreichen, liegt das Geheimnis im IL-Code, der vom C # -Compiler generiert wird. 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-Keyword wirklich?) bietet diese Art von Einblick.

109
John K

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 Sammlungstypen.

Hier ist meine Lösung mit dem Schlüsselwort yield:

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 keine for Schleife über meine Sammlung erstellen, um den Index zu finden. Mein Button hat eine ID und diese wird auch als Index in IndexerList<T> Verwendet, also Sie vermeiden redundante IDs oder Indizes - das gefällt mir! Der Index/die ID kann eine beliebige Zahl sein.

4

Ein praktisches Beispiel finden Sie hier:

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

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

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

Wie Jan_V jedoch sagte (schlagen Sie mich nur um ein paar Sekunden) :-) Sie können ohne es leben, da der Compiler intern in beiden Fällen fast identischen Code erzeugt.

2
Jalayn

Ich habe eine kleine Datenbankdatenschicht mit einer command -Klasse, in der Sie den SQL-Befehlstext und den Befehlstyp festlegen und eine IEnumerable von 'Befehlsparametern' zurückgeben.

Grundsätzlich besteht die Idee darin, CLR-Befehle einzugeben, anstatt die Eigenschaften und Parameter von SqlCommand ständig manuell zu fü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 command Klasse erbt, hat die Eigenschaften Age und Name.

Dann können Sie ein command -Objekt neu auffüllen, dessen Eigenschaften gefüllt sind, und es an eine db -Schnittstelle übergeben, die den Befehlsaufruf tatsächlich ausführt.

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

1
john

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 einem Beispiel der Klasse sagen, dass Bob jeden Wochentag von 9:30 bis 17:30 Uhr arbeitet und um 12:30 Uhr eine Mittagspause einlegt. 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 es folgendermaßen:

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

Die Klasse iteriert nicht wirklich über eine Sammlung. Die Verschiebung zwischen zwei Daten kann jedoch als Sammlung betrachtet werden. Der Operator yield ermöglicht es, diese imaginäre Sammlung zu durchlaufen, ohne die Sammlung selbst zu erstellen.

1
Ant

Obwohl der Zusammenführungsfall bereits in der akzeptierten Antwort behandelt wurde, möchte ich Ihnen die Ertrags-Zusammenführungsparameter-Erweiterungsmethode ™ zeigen:

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 Methode MarkChecksum sieht beispielsweise so aus. Und es hat auch ein 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.

1
Yegor

Elastic Search .NET Beispiel Repo hat ein großartiges Beispiel für die Verwendung von yield return, um eine Sammlung in mehrere Sammlungen mit einer bestimmten Größe zu partitionieren:

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);
        }
    }
1
pholly

Als ich die Antwort von Jan_V erweiterte, traf ich gerade einen realen Fall, der damit zusammenhängt:

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

0
Loren Pechtel