Značky Technorati:
V dnešním díle se zaměříme na dvě důležitá rozhraní - IEnumerable<T> a IQueryable<T> a v závěru se podíváme na spojení klíčových slov yield return.
IEnumerable<T>
foreach (var order in orders)
{
...
}
Určitě každý z vás už nejméně jednou použil foreach. Ale jak je možné, že foreach funguje? Děkovat za to můžeme (kromě programátorům Microsoftu) rozhraní IEnumerable, respektive jeho generické verzi IEnumerable<T>. Toto rozhraní definuje jedinou metodu GetEnumerator, která vrací objekt typu IEnumerator. To obsahuje vlastnost Current a metodu MoveNext. Foreach se za nás postará o veškerou práci s IEnumeratorem a my tak máme práci velmi ulehčenou. Předchozí foreach je interně reprezentován zhruba takto:
E e = ((C)(orders)).GetEnumerator();
try
{
while (e.MoveNext())
{
Order order = (Order)e.Current;
...
}
}
finally
{
// Dispose e
}
Kde e je enumerátor a nelze k němu nijak přistupovat z našeho kódu. Blok finally se liší dle typu E. Kompletní specifikaci foreach najdete v sekci 8.8.4 specifikace jazyka C#.
Za zmínku stojí změna ve verzi C# 5.0. V dřívějších verzích byla proměnná order deklarována mimo tělo cyklu while a do ní přiřazovány hodnoty. Od verze 5.0 je jako v předchozím příkladu deklarována uvnitř cyklu.
Pokud se podíváme na rozhraní IEnumerable<T> blíže pomocí Object Browseru, tak uvidíme, že toto rozhraní je implementováno v dalších více jak 100 typech, za zmínku stojí např. List<T>, string nebo již zmíněné rozhraní IQueryable<T>. Pokud bychom se podívali na negenerickou verzi IEnumerable, tak vězme, že právě z ní je odvozená verze IEnumerable<T>. Také ji implementuje více jak 100 dalších typů, včetně typu Array. Foreach lze použít na jakýkoliv typ implementující jedno z těchto dvou rozhraní.
Protože se v tomto seriálu zabýváme LINQ, tak je pro náš důležitá informace o rozšiřujících (extension) metodách tohoto rozhraní. Je jich opravdu celá řada, mnohé v přetížených verzích. Já zde zmíním nejčastěji používané - Count, Where, OrderBy, Select nebo ToArray, ToList, ToDictionary. Podrobnému popisu jednotlivých metod se budeme věnovat v následujících dílech.
Nyní se podíváme na následující kód:
var numbers = new List<int>(){3, 5, 8, 12, 14, 18, 20, 22};
var result = numbers.Where(num => num > 15);
Console.WriteLine(result.Count());
numbers.Add(19);
Console.WriteLine(result.Count());
Výsledkem bude výpis tři, jelikož máme ve výběru tří čísla (18, 20 a 22). V druhém případě 4. Jak je to možné? Objekt result neuchovává čísla větší než 15, ale náš dotaz. Samotný výpočet výsledků je odložen až na nezbytnou dobu. V našem případě je to volání metody Count. Podobně to ovšem může být metoda ToList nebo použití result v cyklu foreach. V případě, kdy bychom změnili druhý řádek na následující, tak bychom dostali výpis dvakrát za sebou čísla tři.
var result = numbers.Where(num => num > 15).ToList();
Toto je velmi důležitý detail, jak LINQ funguje. Bez této znalosti by se nám mohlo stát, že dostaneme na různých místech neočekávané výsledky. Naopak díky této znalosti můžeme provádět optimalizace kódu.
IQueryable
Jak jsem již zmínil, rozhraní IQueryable je odvozeno od IEnumerable. To znamená, že IQueryable rozšiřuje IEnumerable a cokoliv jde dělat s IEnumerable jde i s IQueryable. IQueryable obsahuje tři vlastnosti: ElementType, Expression, Provider. První určuje jakého typu jsou vracené výsledky, druhá obsahuje expression tree - vysvětlíme si později, třetí obsahuje provider pro zvolený typ dat. Výhodou tohoto rozhraní je, že volané funkce se nevyhodnocují hned, ale staví strom výrazu, který se na konci přeloží do jazyka zdroje dat. Použijme příklad s čísly a představme si, že List numbers je tabulka v databázi obsahující čísla. V případě použití pouze rozhraní IEnumerable by dotaz numbers.Where(num => num > 15) vypadal následovně:
- Dotaz do databáze: SELECT * FROM numbers
- Použití extension metody Where, která vyfiltruje pouze čísla větší než 15
Díky rozhraní IQueryable bude použit rovnou dotaz: SELECT * FROM numbers WHERE number > 15. To může velmi zásadně ovlivnit výkon aplikace, kdy z databáze získáme již vyfiltrovaná dat. K tomu slouží vlastnost Expression.
YIELD RETURN
Yield return si ukážeme rovnou na příkladu. Máme následující metodu GetNumbers:
static IEnumerable<int> GetNumbers()
{
Console.WriteLine("GetNumbers started.");
Console.WriteLine("Returning 1.");
yield return 1;
Console.WriteLine("Returning 2.");
yield return 2;
Console.WriteLine("Returning 3.");
yield return 3;
Console.WriteLine("GetNumbers ending.");
}
GetNumbers vrací IEnumerable<int> s třemi čísly. Čísla si vypíšeme v cyklu:
foreach (var number in GetNumbers())
{
Console.WriteLine("Current number: {0}", number);
}
Program spustíme a podíváme se na výsledek:
GetNumbers started.
Returning 1.
Current number: 1
Returning 2.
Current number: 2
Returning 3.
Current number: 3
GetNumbers ending.
Všimněte si, že jsou vzájemně proložené výpisy z metody GetNumbers a těla cyklu foreach. Yield return totiž funguje tak, že nám přeruší běh metody a provede se tělo cyklu. Při dalším průchodu pokračuje metoda GetNumbers a znovu se její běh přeruší, jakmile narazí na yield return.
Nyní si napíšeme vlastní implementaci metody Where pro IEnumerable<int>. Klasický přístup by vypadal nějak podobně:
public static class Extension
{
public static IEnumerable<int> CustomWhere(this IEnumerable<int> numbers, Func<int, bool> selector)
{
var results = new List<int>();
foreach (var number in numbers)
{
if (selector(number))
results.Add(number);
}
return results;
}
}
Metoda je jednoduchá, vytvoříme si List s výsledky, projdeme všechny hodnoty v cyklu a pokud se nám hodí do výběru, tak je přidáme do výsledků. Ty následně vrátíme. Naší metodu použijeme s jednoduchým lambda výrazem následovně:
numbers = new List<int>(){3, 5, 8, 12, 14, 18, 20, 22};
result = numbers.CustomWhere(num => num > 15);
Naší extension metodu ovšem můžeme zjednodušit použitím yield return:
public static IEnumerable<int> CustomWhere(this IEnumerable<int> numbers, Func<int, bool> selector)
{
foreach (var number in numbers)
{
if (selector(number))
yield return number;
}
}
}
Druhá verze má několik výhod. Nemusíme vytvářet List a metoda je kratší a lépe čitelná. Druhá výhoda je to, že v případě veliké kolekce nemusíme držet všechny výsledky v paměti našeho Listu, ale zpracováváme vždy jeden výsledek.
Klíčové slovo yield můžeme použít i v kombinaci s break. Funguje podobně jako klasický break uvnitř cyklu.
foreach(var item in collection)
{
if(breakCondition)
yield break;
yield return item;
}
Předchozí kód vrátí všechny položky z collection, dokud nenabude proměnná breakCondition hodnoty true.
Shrnutí
Dnes jsme si vysvětlili k čemu jsou a jak fungují dvě důležitá rozhraní IEnumerable a IQueryable, respektive jejich generické verze. Vysvětlili jsme si také rozdíl mezi těmito dvěma rozhraními. V druhé polovině jsme si na příkladu ukázali jak funguje yield return a použili jsme k tomu lambda výrazy a extension metodu, které jsme probrali v minulém díle. Příště navážeme první částí popisu metod rozšiřujících rozhraní IEnumerable.