LINQ: Kontrakt IEnumerable<T>

Tomáš Holan       02.09.2011       LINQ       12813 zobrazení

Dnešní téma bude o něco jednodušší než jsme možná z některých příspěvků zvyklí, ale snad to nebude příliš vadit. Jednou z největších, nejvýznamnější a neužitečnějších inovací, která se nám dostala do jazyka C# během jeho již poměrně dlouhého vývoje, je bezesporu technologie LINQ. Proto je možná trochu škoda, že jsme se na tomto blogu zatím této technologii příliš nevěnovali. Zatím jsme přesto alespoň zmínili záležitosti okolo chování iterátor bloku a deferred execution spolu s rozdílem mezi hot/cold enumerable.

V tomto článku si zopakujeme a shrneme nejzákladnější vlastnosti objektů implementující interface IEnumerable<T>, protože tento interface je jedním z klíčových prvků, nad kterým je celá technologie LINQ vybudována.

Interface IEnumerable<T> je možná nejpoužívanější interface v celém .NETu. Objekt implementující tento interface představuje obecně libovolný pull-based zdroj dat, většinou se jedná např. o list, pole, jiný datový typ reprezentující kolekci, nebo se může jednat o výsledek LINQ dotazu, iterátor nebo nějakou jinou sekvenci.

Objekt implementující IEnumerable<T> tedy buď může data poskytovat sám (např. kolekce, pole), nebo je to objekt, který pouze “ví”, co má dělat v případě, když po něm někdo v budoucnu bude data požadovat (např. LINQ to Objects dotaz).

Přestože toto mohou být velmi odlišné případy, díky interface IEnumerable<T> je ale možné vše toto sjednotit pod jeden datový typ, který pak zajišťuje stejný způsob zpracování (*) nějakým způsobem získaných nebo poskytovaných dat. V jazyku C# je pro zpracování dat IEnumerable zdroje nejčastěji použito klíčové slovo foreach nebo query expressions. Obecně ale platí to samé jako pro každý jiný interface, a to, že by jsme pro jeho používání měli znát jak je definovaný odpovídající kontrakt.

Co mám na mysli tím kontraktem? Kontrakt je dohoda mezi tím, kdo implementuje nějaký objekt a tím, kdo bude daný objekt používat. Dá se představit jako obyčejný popis, který říká jak se obě zúčastněné strany mohou a nemohou chovat. Je to tedy více než pouze čistá definice interface (např.) v jazyku C#. Z vlastní definice interface nevyčteme, že např. metodu B je nutné volat až teprve po předchozím zavolání metody A, v praxi v takovém případě může metoda B např. vyhodit nějakou výjimku, ale také třeba jen nemusí provést danou akci korektně. Všechny takového skutečnosti by měli být uvedeny v dokumentaci daného interface, takže z definice interface a jeho dokumentace by měl být zřejmý i kontrakt daného interface.

A nyní již konkrétně k samotnému IEnumerable<T> resp. dvojici interfaců IEnumerable<T> a IEnumerator<T>. Nejprve si připomeňme jak vypadají jejich deklarace:

public interface IEnumerable<out T>
{
    IEnumerator<T> GetEnumerator();
}

public interface IEnumerator<out T> : IDisposable
{
    T Current { get; }
    bool MoveNext();
}

Z důvodu zajištění větší přehlednosti dalšího pokračování omluvte mírné zjednodušení, kterého jsem se zde úmyslně dopustil. To spočívá v ignorování existence negenerických verzí těchto interfaců, “přesunutí” některých metod z negenerické verze do verze generické a vypuštění metody IEnumerator.Reset(). Jinak řečeno, takto “čistě” by vypadala deklarace těchto interfaců, kdyby nebyli zatíženy postupným vývojem jazyka C# nebo např. kompatibilitou s technologií COM (metoda Reset() byla doplněna z těchto důvodů).

Co nám tedy odpovídající kontrakt říká:

  1. Pokud chceme procházet data z IEnumerable<T> zdroje, nejprve na něm musíme zavolat metodu GetEnumerator(). Tato metoda nám vrátí objekt IEnumerator<T> reprezentující dále celou tuto operaci. Je také dobré si uvědomit, že každé volání této metody nám vždy vrátí nový objekt, který je na ostatních zcela nezávislý a má svůj vlastní stav. Tím nic nebrání v současném přístupu více “klientů”  ke stejnému zdroji (to platí i pro “pouze” single-treadingové scénáře).
  2. Na obdrženém objektu IEnumerator<T> můžeme opakovaně volat metodu MoveNext(), ale jen do doby, kdy nám tato metoda vrátí hodnotu false (zdroj již neobsahuje další prvek) nebo jen do okamžiku, kdy chceme procházení sekvence předčasně ukončit.
  3. Po každém volání MoveNext() vrací vlastnost Current aktuálně “vybíraný” prvek. Přitom platí, že hodnota této vlastnosti před prvním volání MoveNext() nebo v případě, kdy již MoveNext() vrátilo false, není definována.
  4. Metoda MoveNext() nemusí případně vrátit ani hodnotu true, ani hodnotu false, nesmíme totiž zapomenout na to, že kontrakt zahrnuje i případ, kdy tato metoda vyhodí výjimku. V takovém případě celé čtení sekvence končí – chybou, a enumerátor již nelze dále použít (stav objektu je dále nedefinovaný).
  5. Po dokončení procházení sekvence nebo i v případě předčasného ukončení čtení je nutné na enumerátoru zavolat metodu Dispose() pro případné uvolnění všech používaných zdrojů.

Vidíme, že je zde pár podmínek, které je nutné zajistit, což nemusí být ve všech případech vždy úplně samozřejmé.

Dále si ukážeme jak je dodržení tohoto kontraktu zajištěno při použití klíčového slova foreach. Mějme následující kód:

foreach (var item in src)
{
    //Do something with item 
}

Tento kód je při překladu kompilátorem automaticky nahrazen na toto (**):

using (var e = src.GetEnumerator())   //(1)
{
    T item;
    while (e.MoveNext())    //(2)(4)
    {
        item = e.Current;   //(3)
        //Do something with item 
    }
}   //(5)

U jednotlivých řádků je uvedeno, co z výše uvedeného daný řádek zajišťuje.

A nakonec se ještě stručně zmíním o tom, jak interface IEnumerable<T> implementovat. Z pravidla není potřeba implementovat členy tohoto interface přímo (i když je to samozřejmě možné), ale máme dvě další možnosti:

  • Iterátor blok – S tím jsme se samozřejmě již setkali. Jen připomenu, že pomoci iterátor bloku lze implementovat jak IEnumerable<T>, tak i IEnumerator<T>. Pro psaní iterátor bloků také platí nějaká omezení, o kterých se můžete dočíst např. zde (všechny části pak naleznete zde).
  • LINQ – Místo psaní iterátor bloku někdy stačí pouze vytvořit LINQ to Objects dotaz. To má klasické výhody i nevýhody deklarativního programování (specifikujeme co se má udělat místo toho jak se to má udělat, LINQ dotaz “skrývá” cykly). LINQ dotaz se také příliš nehodí pro konstrukci objektů implementující interface IEnumerator<T>.

(*) To platí za předpokladu, že neopustíme náš kontext pull-based modelu.

(**) Všimněte si, že proměnná item je deklarována mimo tělo vlastního cyklu. O tom, jaké to má důsledky se můžete dozvědět zde.

UPDATE:
Způsob generování těla foreach cyklu je nyní v C# 5.0 změněno, viz. tento článek.

 

hodnocení článku

0       Hodnotit mohou jen registrované uživatelé.

 

Nový příspěvek

 

                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:  
Heslo:  

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback