.NET, C# - Používání a psaní enumerátorů
Co tento článek ukazuje
- Jak lze procházet prvky kolekcí pomoci enumerátorů
- Jak se píšou vlastní enumerátory pro procházení vlastních kolekcí
- A jak vše zjednodušuje klíčové slovo
yield return
(v C# 2.0)
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
- [1] Kolekce a seznamy objektů [^]