TP

.NET, C# - Používání a psaní enumerátorů

Co tento článek ukazuje

V .NETu je celá řada kolekcí, do kterých lze ukládat větší počet objektů stejného typu. O tom jaké různé kolekce v .NET Frameworku naleznete jsem psal již dřive [1], nejznámějším je jistě obyčejné pole, dále třída ArrayList a od verze .NET 2.0 i generické třídy, jako například List<T>. U generických kolekcí je místo T možné doplnit nějaký konkrétní typ a získáte tak kolekci která má ve všech metodách (přidávání, získávání prvků) přímo váš typ, takže odpadá přetypovávání, které například u třídy ArrayList bylo nutné používat.

Všechny tyto kolekce umožňují jednotným způsobem přistupovat k prvkům v kolekci a to pomocí takzvaných enumerátorů. Kolekce, která tento přistup podporuje implementuje rozhraní IEnumerable (to vrací jednotlivé položky jako typ object) a nebo modernější (od .NET 2.0) rozhraní IEnumerable<T>, které umožňuje přistupovat k položkám přes jejich skutečný typ.

Enumerátor je jednoduchý objekt, který je vrácen metodou GetEnumerator z kolekce. Tento objekt obsahuje referenci na aktuální prvek v kolekci (při vytvoření ukazuje před začátek kolekce) a pomocí metody MoveNext se dokáže posouvat na další prvky. Tyto enumerátory vnitřně používá i příkaz foreach v jazyce C#, takže pomocí tohoto příkazu lze procházet libovolnou kolekci, která implementuje IEnumerable.

IEnumerator a foreach

Nejprve se tedy podíváme jak se s enumerátory pracuje. Jako příklad použijeme pole (které implementuje negenerickou verzi rozhraní IEnumerable) a třídu List<int>, tedy generický seznam čísel.

// Pole implementuje IEnumerable
int[] collection1 = new int[] { 1,2,3,4,5 };

// List<T> implementuje IEnumerable<T>
List<int> collection2 = new List<int>();
collection2.Add(1);
collection2.Add(2);
collection2.Add(3);
collection2.Add(4);
collection2.Add(5);

// prochazeni kolekce pomoci foreach
foreach(int n in collection1)
{
  Console.Write(n);
}

Teď máme vytvořené dvě kolekce pro testovací účely. Procházení kolekcí pomocí příkazu foreach v předcházející ukázkce jistě není pro nikoho nové. Ve skutečnosti je ale foreach pouze zkratka za konstrukci, kterou můžete vidět v následujícím příkladě:

IEnumerator<int> enumerator2 = collection2.GetEnumerator();
while(enumerator2.MoveNext())
{
  Console.Write(enumerator2.Current);
}

Je vidět, že pro procházení se pomocí metody GetEnumerator nejprve vytvoří instance enumerátoru. Tento objekt má metodu MoveNext(), pomocí které se posouvá na další prvek v kolekci a vlastnost Current, přes kterou lze přistupovat k aktuálnímu prvku na který enumerátor odkazuje. Po vytvoření enumerátor ukazuje před začátek kolekce a až prvním voláním metody MoveNext() se dostane na první prvek v kolekci. Procházení kolekce skončí ve chvíli, kdy tato metoda vrátí false, což znamená, že jsme došli na konec seznamu.

Vlastní kolekce a enumerátory

Pokud píšete vlastní kolekci pro ukládání dat a chcete aby s ním bylo možné pracovat standardním způsobem (tedy například procházet pomocí konstrukce foreach), tak budete potřebovat vytvořit také enumerátor. V současné době existují dva způsoby jak toto udělat. První, který vám dává plnou kontrolu nad tím, jak se enumerátor chová, je napsat vlastní třídu implementující rozhraní IEnumerable. Tento způsob je poměrně zdlouhavý a proto v C# 2 přibyla druhá možnost a to napsat pouze metodu, která vrací postupně jednotlivé prvky z kolekce.

Protože cílem tohoto článku je vysvětlit jak enumerátory fungují, podíváme se nejprve na zdlouhavější metodu, kterou bylo potřeba používat až do příchodu C# 2. Pro příklad vytvoříme kolekci, která ukládá čísla v zřetězeném seznamu. Seznam je složený z objektů typu Node, které v sobě obsahují nějaká data (číslo) a odkaz na další buňku seznamu. Struktura pro jednotlivé buňky vypadá následujícím způsobem:

class Node
{
  // data v node
  public int Value;
  // dalsi node v retezu
  public Node Next;
}

Kompletní implementaci seznamu naleznete v přiložených zdrojových kódech. Pro tento článek důležitá část kódu kolekce je implementace rozhraní IEnumerable tedy metody GetEnumerator(), která vrací enumerátor:

class CustomCollection1 : IEnumerable<int>
{
  // prvni node
  Node _first;

  // Vytvari kolekci z pole integeru
  public CustomCollection1(int[] prvky)
  {
    // ...
  }

  // Vraci enumerator - tedy objekt pomoci ktereho se
  // bude prochazet prez prvky kolekce
  public IEnumerator<int> GetEnumerator()
  {
    return new MyEnumerator(_first);
  }

  // Z duvodu zpetne kompatibility je krom rozhrani 
  // IEnumerable<int> implementovat i IEnumerable
  IEnumerator IEnumerable.GetEnumerator()
  {
    return GetEnumerator();
  }
}

Tato ukázkové kolekce implementuje generickou verzi rozhraní a proto je potřeba implementovat metodu, která vrací enumerátor implementující IEnumerator<T>. Kromě této metody je ale z důvodu zpětné kompatibility potřeba přidat i negenerickou verzi metody. Její implementace je ale velmi jednoduchá, protože stačí vracet objekt vytvářený generickou metodou. Tento příklad tedy ukazuje jakým způsobem lze napsat kolekci, ale stále ještě chybí to nejdůležitější - to jest objekt implementující enumerátor. V kódu se již vyskytuje objekt MyEnumerator, který v konstruktoru bere jako parametr referenci na první prvek seznamu. Nyní se tedy podíváme, jak lze implementovat tento objekt a jakým způsobem v něm projít všechny prvky.

// Enumerator ktery prochazi kolekci objektu 
class MyEnumerator : IEnumerator<int>
{
  // Reference na prvni a aktualni prvek seznamu
  Node _first, _current=null;
  
  // Vytvari enumerator - parametr je reference na zacatek kolekce
  public MyEnumerator(Node node)
  {
    _first = node;
  }

  // Vraci aktualni prvek
  public int Current 
  {
    get { return _current.Value; } 
  }

  // Vraci aktualni prvek jako objekt (kvuli .NET 1.0 enumeratorum)
  object IEnumerator.Current 
  {
    get { return Current; } 
  }

  // Posouva enumerator na dalsi hodnotu
  // Pri prvnim volani by mela nastavit Current na prvni hodnotu
  public bool MoveNext()
  {
    if (_current == null) 
      // prvni volani
      _current = _first;
    else
      // posun na dalsi prvek
      _current = _current.Next;

    return _current != null;
  }

  // Reset enumeratoru pred zacatek
  public void Reset()
  {
    _current = null;
  }

  // Enumeratory jsou IDisposable
  public void Dispose() {}
}

Enumerátory v C# 2

Nyní jsme si ukázali jak lze napsat vlastní kolekci s vlastním enumerátorem, takže je na procházení kolekcí možné používat příkaz foreach. To že psaní takovéto třídy není úplně nejjednodušší si uvědomili i autoři jazyka C# a proto v nové verzi poskytují mnohem lepší způsob pro psaní enumerátorů. Řešení ukázané výše samozřejmě lze použít i v C# 2, ale kromě něj přibyla druhá možnost.

Pokud si pozorně prohlédnete kód, který je potřeba pro psaní enumerátoru, zjistíte, že jediná skutečně důležitá část kódu je funkce MoveNext, která posouvá enumerátor na další prvek. V C# 2 existuje možnost, napsat funkci, která vrací IEnumerator, ale místo toho, aby vracela instanci nějakého objektu vrací postupně jednotlivé prvky z kolekce. Kompilátor poté vygeneruje třídu - enumerátor za vás. Následující kód ukazuje enumerátor z předcházejícího příkladu napsaný v C# 2:

// Vraci postupne vsechny prvky kolekce
public IEnumerator<int> GetEnumerator()
{
  Node current = _first;
  while(current != null) 
  {
    yield return current.Value;
    current = current.Next;
  }
}

Tím se příklad celkem podstatně zjednodušuje. Používá se zde nové klíčové "dvouslovo" yield return, pomocí kterého lze postupně vracet jednotlivé prvky. Prvky se vracejí až ve chvíli kdy jsou potřeba, takže pokud budete takovouto kolekci procházet pomocí foreach, doběhne program k prvnímu volání yield return, poté se provede kód uvnitř foreach cyklu a následně se, při načítání dalšího prvku z enumerátoru, opět vrátí program do této funkce a proběhne další cyklus v enumerátoru až do dalšího volání yield return. Celé procházení se opakuje až dokud metoda GetEnumerator neskončí, protože poté vygenerovaný enumerátor vrátí z metody MoveNext hodnotu false a procházení kolekce je tím ukončeno.

Soubory na stažení a odkazy

Published: Sunday, 21 May 2006, 3:14 AM
Author: Tomas Petricek
Typos: Send me a pull request!
Tags: