C# im Fokus

Implementieren eigener Iteratoren Iterator & yield
Kommentare

Das Durchlaufen von Ergebnismengen mittels Schleifen gehört zu den täglichen Aufgaben eines Entwicklers. Werden selbst Ergebnisse in Listenform zurückgegeben, geschieht das meist unter Verwendung eines Listentyps. Teilweise ist es aber effizienter, eigene berechnete Ergebnisse iterationsfähig an den Aufrufer zurückzugeben. Der wesentliche Vorteil bei der Verwendung eines Iterators als Rückgabetyp statt als Listentyp, liegt in der frühen Verfügbarkeit der Ergebnisse.

Ein Iterator ermöglicht den sofortigen Zugriff auf Elemente, sobald diese verfügbar sind. Wird dagegen eine Liste zurückgegeben, so muss zunächst intern die gesamte Liste aufgebaut werden. Das heißt der Zugriff auf verarbeitete Elemente ist erst dann möglich, wenn die gesamte Liste vollständig verfügbar ist. Im Folgenden wird zunächst die Umsetzung einer iterationsfähigen Klasse dargestellt. Um eigene Klassen iterationsfähig zu machen, müssen diese entweder die Schnittstellen IEnumeratorund IEnumerableoder IEnumerable und IEnumerator implementieren. Listing 1 zeigt dazu ein einfaches Beispiel, wie diese Schnittstellen zu implementieren sind. Die Schnittstelle IEnumerable verlangt die Umsetzung der Methode GetEnumerator. Der Rückgabewert der Methode muss vom Typ IEnumerator sein. Da die Klasse ebenfalls die Schnittstelle IEnumerator implementiert, gibt die Methode per Schlüsselwort this die aktuelle Instanz zurück. Die IEnumerator-Schnittstelle verlangt die Implementierung der drei Methoden Current, MoveNext und Reset. Current gibt das aktuelle Objekt der internen Auflistung zurück und MoveNext bewegt den internen Positionszeiger (im Beispiel die Variable position) zum nächsten Element. Reset ermöglicht das Zurücksetzen des Positionszeigers auf den Anfang der Auflistung.

Generische Iteratoren

Das Beispiel in Listing 1 demonstrierte die Umsetzung einer iterationsfähigen Klasse. Dabei wurden nicht die generischen Gegenstücke der Schnittstellen IEnumerable und IEnumerator verwendet. In der Regel ist es aber sinnvoller, die generischen Schnittstellen zu implementieren. Somit entfallen notwendige Typumwandlungen, da der Iterator direkt den korrekten Typ zurückliefert. Listing 2 zeigt im Vergleich, wie das bereits im Listing 1 gezeigte Beispiel unter Verwendung der generischen Schnittstellen realisiert werden kann. Die notwendigen Änderungen sind nur marginal. Current gibt nun statt einem object einen speziellen Typen zurück, und der Rückgabewert von GetEnumerator wurde ebenfalls angepasst.

Listing 1

public class Contacts : IEnumerable, IEnumerator
{
  private Contact[] contacts = null;
  private int position = 0;
  public Contacts(Contact[] contacts)
  {
    this.contacts = new Contact[contacts.Length];
    int index = 0;
    foreach (var item in contacts)
      this.contacts[index++] = item;
  }
  public IEnumerator GetEnumerator()
  {
    return this;
  }
  public object Current
  {
    get
    {
      return contacts[position];
    }
  }
  public bool MoveNext()
  {
    position++;
    return (position < contacts.Length);
  }
  public void Reset()
  {
    position = -1;
  }
}  

Listing 2

public class Contacts : IEnumerable, IEnumerator
{
  private Contact[] contacts = null;
  private int position = 0;
  public Contacts(Contact[] contacts)
  {
    this.contacts = new Contact[contacts.Length];
    int index = 0;
    foreach (var item in contacts)
      this.contacts[index++] = item;
  }
  public IEnumerator GetEnumerator()
  {
    return this;
  }
  IEnumerator IEnumerable.GetEnumerator()
  {
    return this;
  }
  public Contact Current
  {
    get
    {
      return contacts[position];
    }
  }
  object IEnumerator.Current
  {
    get
    {
      return contacts[position];
    }
  }
  public void Dispose() { }
  public bool MoveNext()
  {
    position++;
    return (position < contacts.Length);
  }
  public void Reset()
  {
    position = -1;
  }
}  

Listen vs. Enumerator

Listen werden, wie bereits erwähnt, als Rückgabewerte verwendet. Das ist nicht immer optimal, da in diesem Fall der Aufrufer erst auf die Ergebnisse zugreifen kann, wenn die gesamte Ergebnismenge zur Verfügung steht. Besser ist es, ein Zugriff auf (berechnete) Ergebnisse so früh wie möglich zu gewähren. Denn teilweise benötigt der Aufrufer nicht alle Ergebnisse und verarbeitet nur die Elemente aus der Liste bis zu einem bestimmten Abbruchkriterium. Die verbleibenden Elemente aus der Liste werden also unter Umständen gar nicht mehr berücksichtigt. Das ist besonders tragisch, wenn der Aufbau der Liste viel Zeit in Anspruch genommen hat. Das Beispiel in Listing 3 verdeutlicht das Problem. Die Methode GetNodeInfoList liefert eine Liste aller Artikel aus einer XML-Datei. Zu jedem gelesenen Artikel werden über einen Onlineservice die aktuellen Preisinformationen bezogen und aktualisiert (GetPriceInfo). GetNodeInfoList verarbeitet dazu zunächst alle Artikel aus der Datei und gibt die gesamte Ergebnismenge als Liste zurück. GetNodeInfoEnum hingegen liest einen Artikel aus der XML-Datei, aktualisiert die Preisinformation und gibt danach sofort den aktualisierten Artikel zurück. Zunächst scheint dieses Vorgehen keinen großen Unterschied darzustellen, beide Methoden liefern das gleiche Ergebnis. Ein Geschwindigkeitsunterschied ist jedoch merklich zu spüren, wenn der Aufrufer der Methode nur einen kleinen Teil der Daten benötigt. Das verdeutlichen die beiden foreach-Schleifen am Ende von Listing 3. Bei der Verwendung der ersten Methode muss zunächst die gesamte Liste abgerufen werden. Danach können die gesamten Inhalte durchlaufen werden. Das bedeutet, dass zunächst alle Artikel vollständig verarbeitet werden mussten. In der Schleife werden aber nur Artikel benötigt, bis eine bestimmte Artikelnummer erreicht wurde, die restlichen werden ignoriert. Die zweite foreach-Schleife verwendet die Enumerator-Methode und kann somit sofort auf verfügbare Artikel zugreifen. Wird die Schleife mittels break unterbrochen, werden die restlichen Artikel auch nicht mehr verarbeitet. Somit wird die zweite Variante wesentlich schneller ausgeführt als die erste.

Listing 3

public List GetNodeInfoList()
{
    List retList = new List();
    XmlDocument dom = new XmlDocument();
    dom.Load(...);
    XmlNodeList list = dom.SelectNodes("//color_swatch");
    foreach (XmlNode node in list)
        retList.Add( GetPriceInfo(node) );
    return retList;
}
public IEnumerable GetNodeInfoEnum()
{
    List retList = new List();
    XmlDocument dom = new XmlDocument();
    dom.Load(...);
    XmlNodeList list = dom.SelectNodes("//color_swatch");
    foreach (XmlNode node in list)
        yield return GetPriceInfo(node);
}
foreach (XmlNode node in GetNodeInfoList())
{
    if (ToInt(node.Attributes["image"].Value)>100)
        break;
    // verarbeite ...
}
foreach (XmlNode node in ci.GetNodeInfoEnum())
{
    if (ToInt(node.Attributes["image"].Value)>100)
        break;
    // verarbeite ...
}  
Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -