Yield return & using IDisposable

Author:

English Version

“Yield return” ist ein mächtiges und praktisches Statement, wenn man schnell und einfach eine iterierbare Liste erzeugen möchte, ohne erst großartig ein Array oder eine List anzulegen:

using System;
using System.Collections.Generic;
using System.Drawing;

class Program
{
    static void Main()
    {
        var colors = Rainbow;

        Console.WriteLine("colors.GetType(): {0}", colors.GetType());
        Console.WriteLine();

        foreach (Color color in colors)
        {
            Console.WriteLine(color.Name);
        }

        Console.ReadLine();
    }

    static IEnumerable<Color> Rainbow
    {
        get
        {
            yield return (Color.Red);
            yield return (Color.Orange);
            yield return (Color.Yellow);
            yield return (Color.Green);
            yield return (Color.LightBlue);
            yield return (Color.Indigo);
            yield return (Color.Violet);
        }
    }
}
Red
Orange
Yellow
Green
LightBlue
Indigo
Violet

Der .NET Compiler erzeugt dann im Hintergrund die notwendigen IEnumerable- und IEnumerator-implementierenden Klassen und die State Machine und alles ist fein und übersichtlich.

Eine andere Anwendungsart wäre, wenn man eine existierende List irgendwie in eine andere List transformieren müsste, zum Beispiel eine List<Color> in eine List<String>:

using System;
using System.Collections.Generic;
using System.Drawing;

class Program
{
    static void Main()
    {
        var colors = Rainbow;

        Console.WriteLine("colors.GetType(): {0}", colors.GetType());
        Console.WriteLine();

        var colorNames = GetColorNames(colors);

        Console.WriteLine("colorNames.GetType(): {0}", colorNames.GetType());
        Console.WriteLine();

        foreach (String colorName in colorNames)
        {
            Console.WriteLine(colorName);
        }

        Console.ReadLine();
    }

    static IEnumerable<String> GetColorNames(IEnumerable<Color> source)
    {
        foreach (Color color in source)
        {
            yield return (color.Name);
        }
    }

    static IEnumerable<Color> Rainbow
    {
        get
        {
            yield return (Color.Red);
            yield return (Color.Orange);
            yield return (Color.Yellow);
            yield return (Color.Green);
            yield return (Color.LightBlue);
            yield return (Color.Indigo);
            yield return (Color.Violet);
        }
    }
}

Wobei hier wirklich zu überdenken wäre, ob die Linq-Funktion Select() nicht adäquater eingesetzt wäre:

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;

class Program
{
    static void Main()
    {
        var colors = Rainbow;

        Console.WriteLine("colors.GetType(): {0}", colors.GetType());
        Console.WriteLine();

        var colorNames = Rainbow.Select(color => color.Name);

        Console.WriteLine("colorNames.GetType(): {0}", colorNames.GetType());
        Console.WriteLine();

        foreach (String colorName in colorNames)
        {
            Console.WriteLine(colorName);
        }

        Console.ReadLine();
    }

    static IEnumerable<Color> Rainbow
    {
        get
        {
            yield return (Color.Red);
            yield return (Color.Orange);
            yield return (Color.Yellow);
            yield return (Color.Green);
            yield return (Color.LightBlue);
            yield return (Color.Indigo);
            yield return (Color.Violet);
        }
    }
}

Gefährlich wird “yield return” aber dann, wenn Klassen ins Spiel kommen, die IDisposable implementieren. Denn hier muss man dann genau aufpassen, was der Compiler genau macht und ob man seine Methoden richtig zugeschnitten hat. Oder ob es sinnvoller wäre, an dieser Stelle auf “yield return” zu verzichten. “Yield return” setzt wie Linq auf verzögerte Ausführung (deferred execution), was bedeutet, dass der Code in der Methode nicht zu dem Zeitpunkt ausgeführt wird, in dem die Methode aufgerufen wird, sondern erst, wenn der Iterator seine Arbeit beginnt. Zu diesem Zeitpunkt kann es aber sein, dass die IDisposable-Klasse schon längst disposed wurde und damit der Iterator ins Leere greift.

Um mal deutlich zu machen, was der Compiler bei einem “yield return” eigentlich treibt und warum es relevant ist, in welchem Zusammenhang der IDisposable und der Iterator stehen, habe ich mal zwei Beispielapplikationen implementiert. Die Erste benutzt “yield return” einmal nicht erfolgreich und dann in diversen Abwandlungen erfolgreich. Die Zweite zeigt auf, was der Compiler eigentlich genau macht, in dem ich den IEnumerable und IEnumerator einfach selbst implementiert habe, analog dazu, wie es der Compiler für “yield return” tun würde.

Inhaltlich verhalten sich beide Programme gleich, so dass man schön debuggen kann, wo der Unterschied zwischen einem funktionierenden und einem fehlerhaften “yield return” liegen.

yield return (mit Methodenaufruf-Logging)

IEnumerable / IEnumerator (mit Methodenaufruf-Logging)