Viele Änderungen sind revolutionär, andere eher aus einer gewissen Logik heraus entstanden, also evolutionär. Manche Änderungen machen das Leben viel leichter, andere nur ein wenig. Das yield-Schlüsselwort ist eines der Dinge, daß Dir ein wenig Arbeit sparen kann, und auf lange Sicht ist das eine gute Sache. In diesem Artikel zeige ich, wie das yield-Schlüsselwort helfen kann, den Footprint eines C#-Programms zu verringern und gleichzeitig kleine Probleme beseitigt.
Mittels CodeDOM können nette lustige Dinge mit .NET angestellt werden und das Schlüsselwort yield ist aus dieser Disziplin hervorgegangen. Die Paarungen yield return und yield break sparen uns ein wenig Zeit und Aufwand, und dazu sparen wir der Run-Time das häufige Kopieren von Sublists.
Collections und Sublisten
Eines der häufigsten Code-Fragmente sind Schleifen (Loops). Sie sind überall und tatsächlich so geläufig, daß es sogar ein Behavior Pattern, nämlich das Iterator-Pattern, welches uns maßgeblich von einer Menge Arbeit befreit.
In .NET unterstützen IEnumerable, IEnumerator und IEnumerable
Das Iterator-Pattern in .NET implementiert zu haben ist in jedem Fall eine gesunde aber auch nicht weiter tragische Erfahrung, die jeder Programmierer einmal gemacht haben sollte. So ein Pattern müssen wir nicht notwendigerweise immer wieder für jede Anwendung neu Implementieren, aber grundsätzlich erspart sie uns das Schreiben unnötigen und fehlerträchtigen Codes.
Ein zweite, brilliante Erweiterung ist das yield-Schlüsselwort. Was das Schlüsselwort für uns bedeutet erfahren wir am Besten, wenn wir uns anschauen, wie wir foreach für das Kopieren von Listen und Sublisten bisher verwendet haben, und hinterher lernen, was yield für uns tun kan.
In den häufigsten Fällen wollen wir mit Loops etwas kleines innerhalb eines großen finden. Möchten wir beispielsweise die Postleitzahl einer Kundenadresse innerhalb eine Liste von Kunden-Objekten finden, müssen wir die Liste durchsuchen und jedes Objekt nach der PLZ fragen.
Listing 1 zeigt eine Klasse zur Verwaltung von Ereignissen in Bezug auf Termine und nicht in der .NET-Terminologie, während Listing 2 illustriert, wie eine Sublist von Event-Objekten mittels List
Listing 1: Eine einfache Ereignis-Klasse
namespace YieldTest { public class Appointment { private String description; private DateTime occurs; public Appointment(DateTime occurs, String description) { this.occurs = occurs; this.description = description; } public DateTime Occurs { get { return occurs; } set { occurs = value; } } public String Description { get { return description; } set { description = value; } } public override string ToString() { return string.Format("{0}: {1}", occurs, description); } } }
Listing 2: Klasse, die eine Subliste mit Elementen zurückgibt.
using System; using System.Collections.Generic; using System.Text; namespace YieldTest { class Program { static void Main(String[] args) { List<appointment> app = new List<appointment>(); List<appointment> may; app.Add(new Appointment(new DateTime(2007, 5, 6), "Meet Lucy")); app.Add(new Appointment(new DateTime(2007, 6, 15), "Rent car")); app.Add(new Appointment(new DateTime(2007, 6, 15), "Mom's Birthday")); app.Add(new Appointment(new DateTime(2007, 5, 1), "Concert")); may = GetMayEvents(app); foreach (Appointment e in may) Console.WriteLine(e); Console.ReadLine(); } // 97 bytes MSIL-Code public static List<appointment> GetMayEvents(List<appointment> eventList) { List<appointment> may = new List<appointment>(); foreach (Appointment e in eventList) if (e.Occurs.Month == 5) may.Add(e); return may; } } }
Solcher Code existiert in vielen Millionen Programmen in vielfältiger Weise. Abgesehen davon, daß er ziemlich viel Platz benötigt, obwohl wie effektiv nur 5 Zeilen Code geschrieben haben (Abbildung 1) ist ein Problem, das andere ist, daß wir diesen Code immer und immer wieder schreiben müssen, je nach Anwendungsfall.

MSIL-Code für die Funktion GetMayEvents: der herkömmliche Weg benötigt viel Code, hier: 97 Bytes.
yield erzeugt dynamische Listen
Listing 2 erzeugt eine Untermenge aus einer Liste und gibt sie zurück. Was wir aber eigentlich wollen, ist die Möglichkeit über diese Untermenge iterieren zu können. Genau diese Funktion liefert yield.
In dem wir yield verwenden, brauchen wir keine Subliste erstellen und müssen damit auch keine Referenzen in die zweite Liste stopfen. Das yield-Schlüsselwort übernimmt das alles für uns, wie Listing 3 zeigt.
Listing 3: Die Funktion GetJuneEvents verwendet yield.
public static IEnumerable<appointment> GetJuneEvents(List<appointment> events) { foreach (Appointment e in events) if (e.Occurs.Month == 6) yield return e; }
OK, yield ist eigentlich kein eigenständiges Schlüsselwort, Du kannst es also auch als Variablennamen verwenden, aber da es eine spezielle Funktion in bestimmten Kontexten erfüllt, bleibe ich bei der Bezeichnung Schlüsselwort. yield muß return oder break vorangestellt werden.
Der neue Code sieht zugegebenermaßen irgendwie komisch aus. Kehrt die Funktion nicht jedesmal zurück, wenn ein Event im April gefunden wurde? Nein, obwohl es in der Tat so aussieht. Was passiert ist, daß beim Antreffen von yield return das Current-Property der Iterators (IEnumerable) auf das Objekt im yield-Statement gesetzt, der Funktionszustand auf dem Stack gespeichert und die Funktion verlassen wird, außer in Loops. Hier wird beim nächsten Aufruf von foreach der Funktionszustand wiederhergestellt, IEnumerable.Current auf das nächste Element gesetzt und so weiter. Im Endeffekt erstellt yield für uns eine dynamische enumerierbare Liste.
Um die Ereignisse im Juni abfragen zu können müssen wir das Hauptprogram lediglich um ein wichtiges Detail erweitern:
Listing 4: Der vollständige Code mit und ohne yield.
namespace YieldTest { class Program { static void Main(String[] args) { List<appointment> app = new List<appointment>(); IEnumerable<appointment> list; app.Add(new Appointment(new DateTime(2007, 5, 6), "Meet Lucy")); app.Add(new Appointment(new DateTime(2007, 6, 15), "Rent car")); app.Add(new Appointment(new DateTime(2007, 6, 15), "Mom's Birthday")); app.Add(new Appointment(new DateTime(2007, 5, 1), "Concert")); list = GetMayEvents(app); foreach (Appointment e in list) Console.WriteLine(e); list = GetJuneEvents(app); foreach (Appointment e in list) Console.WriteLine(e); Console.ReadLine(); } // 97 bytes MSIL-Code public static List<appointment> GetMayEvents(List<appointment> events) { List<appointment> may = new List<appointment>(); foreach (Appointment e in events) if (e.Occurs.Month == 5) may.Add(e); return may; } // 21 bytes MSIL-Code public static IEnumerable<appointment> GetJuneEvents(List<appointment> events) { foreach (Appointment e in events) if (e.Occurs.Month == 6) yield return e; } } }
Da List

MSIL-Code für die Funktion GetMayEvents: mittels yield reduziert sich der funktional identische Code drastisch, hier: 21 Bytes.
Predicate bringt maximale Flexibilität
Natürlich ist die neue Funktion mit yield chic, aber dennoch nicht besonders flexibel. Fügen wir aber ein Predicate
Listing 5: Der neue Code mit Predicate.
static void Main(String[] args) { List<appointment> app = new List<appointment>(); IEnumerable<appointment> list; app.Add(new Appointment(new DateTime(2007, 5, 6), "Meet Lucy")); app.Add(new Appointment(new DateTime(2007, 6, 15), "Rent car")); app.Add(new Appointment(new DateTime(2007, 6, 15), "Mom's Birthday")); app.Add(new Appointment(new DateTime(2007, 5, 1), "Concert")); // neuer Ansatz mit yield und Predicate foreach (Appointment e in GetEvents(app, Match)) Console.WriteLine(e); Console.ReadLine(); } private static bool Match(Appointment a) { return a.Occurs.Month == 5; } public static IEnumerable<appointment> GetEvents( List<appointment> events, Predicate<appointment> match ) { foreach (Appointment e in events) if (match(e)) yield return e; }
Wie wir sehen können ist der Code zwar auf den ersten Blick etwas länger aber viel flexibler, da wir nur noch eine Funktion anpassen müssen, um die Methode zu testen. Außerdem können wir auch statt GetEvents auch einfach List
Zusammenfassung
yield ist ohne return oder break kein Schlüsselwort. In iterativen Schleifen muß yield return verwendet werden. Desweiteren darf yield nicht in anonymen Methoden verwendet werden, unsicherer Code (unsafe) ist ebenfalls tabu und ref und out dürfen nicht als Variablen für yield verwendet werden. Und schließlich darf yield nicht in einem catch-Block oder mit einem try-Block mit mehr als einem catch-Block vorkommen.
Weiterführende Informationen sind in Bill Wagners exzellentem Artikel über Prädikate, Iteratoren und Generics auf MSDN zu finden.
In unserem Fall genügt es zu sagen, daß wann immer wir Listen durchlaufen müssen, wir überlegen sollten, ob wir nicht besser mit yield arbeiten, da es mehr Flexibilität und Leistungszuwachs bringt, ohne den Code komplexer zu machen. Das erreichen wir, weil yield einen dynamischen Iterator zur Laufzeit erzeugt und uns damit eine Menge Code sparen kann.
graegerts