V C# verze 2.0 se objevilo klíčové slovo yield, které je dle mého skromného názoru velmi mocné, ale co jsem si všiml, tak se netěší mezi vývojáři takové popularitě, jakou by si zasloužilo. Dost možná za to může mlha záhadnosti, která ho obestírá. Snad se mi podaří tímto článku onu mlhu rozkrýt – povíme si něco o yieldování v C#.
V C# 2.0 přibyla možnost vytvářet tzv. iterátory, což je buď metoda, getter nebo operátor, který vrací typ IEnumerable nebo jeho generickou variantu IEnumerable<T>. Iterátor nám umožní to, že z této metody (getteru, operátoru) nemusíme explicitně vracet nějakou třídu implementující toto rozhraní (List<T>, pole), jak bývá zvykem. Místo toho v metodě popíšeme pomocí yield return a příp. yield break samotný algoritmus iterování. Konkrétně pomocí yield return vracíme další prvek a pomocí yield break říkáme, že už není dostupný žádný další prvek. Překladač udělá špinavou práci za nás a na pozadí vygeneruje pomocnou třídu, která správně implementuje IEnumerable, příp. IEnumerable<T>, tedy metodu MoveNext() atd.
Dost bylo teorie, koukněme se na příklad z MSDN:
using System;
using System.Collections;
using System.Collections.Generic;
public static class Program
{
// Budeme postupně vracet mocniny daného čísla.
public static IEnumerable<int> Power(int number, int exponent)
{
int counter = 0;
int result = 1;
while (counter++ < exponent)
{
result = result * number;
// v tomto okamžiku říkáme, že další položkou k enumerování
// je aktulální hodnota proměnné result
yield return result;
}
}
static void Main()
{
foreach (int i in Power(2, 8))
{
Console.Write("{0} ", i);
// 2 4 8 16 32 64 128 256
}
}
}
Praktické využití této vymoženosti spočívá hlavně v chytřejší práci s kolekcemi. Dejme tomu, že přes sebou máme následující jednoduchý, avšak typický, úkol. Z datové vrstvy vycucnout nějaká data a ty v business vrstvě nějak zpracovat. Naivní implementace by mohla vypadat takto:
public static List<DTO> GetObjects()
{
using(var connection = DI.GetConnection())
{
using(var command = connection.CreateCommand())
{
command.CommandText = "SELECT column1, column2 FROM table1";
using(var reader = command.ExecuteReader())
{
var res = new List<DTO>();
while(reader.Read())
{
res.Add(new DTO { Value1 = reader["column1"], Value2 = reader["column2"] });
}
return res;
}
}
}
}
public static void ProcessObjects(IEnumerable<DTO> objects)
{
foreach (var item in objects)
{
// process object
}
}
Důležitý je okamžik, kdy vytvoříme nějakou kolekci (v tomto případě List<DTO>), naplníme ji a nakonec ji vrátíme.
Pokud budeme pracovat s málo objekty, všechno bude relativně v pohodě. Co ale pokud budeme pracovat s tolik záznamy, že se nám to nevleze do paměti? Pak by se nám hodilo nějaké proudové zpracování. A právě jeho implementaci nám usnadní yieldování.
public static IEnumerable<DTO> GetObjects()
{
using(var connection = DI.GetConnection())
{
using(var command = connection.CreateCommand())
{
command.CommandText = "SELECT column1, column2 FROM table1";
using(var reader = command.ExecuteReader())
{
while(reader.Read())
{
yield return new DTO { Value1 = reader["column1"], Value2 = reader["column2"] };
}
}
}
}
}
Jak vidno, yield je tedy výhodné použít v mnoha případech, kdy vracíme nějakou kolekci.
V předchozích příkladech jsem použil pouze konstrukci yield return. Máme k dispozici ještě yield break, který slouží k ukončení iterace, tudíž k vyskočení z metody – nic složitého.
Spíše jako zajímavost přihodím ještě jedno použití. Dejme tomu, že máme nějakou metodu, která nám vrací kolekci prvků. Vrácení jednoho prvku je časově poměrně náročná operace, ale zároveň ne tak náročná, aby nás to žralo. Pokud bychom vraceli normální kolekci (List<T>, pole), pravděpodobně bychom metodu napsali nějak chytře asynchronně apod., protože při vrácení většího počtu prvků najednou by nás prodleva už mohla začít žrát a my bychom chtěli v mezičase třeba překreslovat GUI. S yieldem ale nic takového není třeba. Ukážeme si to v praxi na implementaci příkazu Traceroute, který nedělá nic jiného, než že posílá ICMP pakety ze vzrůstajícím TTL (time-to-live) použitého IP paketu.
public class TracerouteResult
{
public System.Net.IPAddress Address { get; set; }
public long RoundtripTime { get; set; }
}
public static IEnumerable<TracerouteResult> Traceroute(System.Net.IPAddress destination, int timeout, int maxHops)
{
using (var ping = new System.Net.NetworkInformation.Ping())
{
int errors = 0;
var dataToSend = new byte[32];
//var res = new List<TracerouteResult>(maxHops);
for (int ttl = 2; ttl < maxHops + 2; )
{
var reply = ping.Send(destination, timeout, dataToSend, new System.Net.NetworkInformation.PingOptions(ttl, true));
if (reply.Status == System.Net.NetworkInformation.IPStatus.TimedOut)
{
yield break;
}
if (reply.Status == System.Net.NetworkInformation.IPStatus.Success ||
reply.Status == System.Net.NetworkInformation.IPStatus.TtlExpired)
{
//res.Add(new TracerouteResult { Address = reply.Address, RoundtripTime = reply.RoundtripTime });
yield return new TracerouteResult { Address = reply.Address, RoundtripTime = reply.RoundtripTime };
if (reply.Status == System.Net.NetworkInformation.IPStatus.Success)
{
yield break;
}
ttl++;
errors = 0;
continue;
}
// another status
if (++errors > 10)
{
yield break;
}
}
//return res;
}
}
static void Main(string[] args)
{
foreach (var reply in Traceroute(System.Net.Dns.GetHostEntry("www.augi.cz").AddressList[0], 1000, 32))
{
Console.WriteLine("{0}t{1} ms", reply.Address, reply.RoundtripTime);
}
Console.Read();
}
Kdybychom uvnitř metody Traceroute pouze tupě naplnili kolekci, dostali bychom celý výsledek najednou až zpracování posledního paketu. Díky yieldu můžeme vracet výsledky postupně. Dokonce by metoda nemusela mít parametr maxHops, ale mohli bychom tuto logiku přenést na uživatele naší metody Traceroute – když už by bylo výsledků dost, jednoduše by přestal iterovat.
Umm, nutno podotknout ze typ Enumerable neni collection a chova se zcela jinak. Yield se hodi tam, kde provadime s kolekci jednorazovou operaci, pokud chceme s kolekci dale pracovat, yield je spatne reseni a ani pouzit nejde. Zasadni rozdil je, ze pri prochazeni Enumerable se kod v return yield provadi znova a znova pri kazdem pruchodu.
Tzn napriklad priklad s GetObjects() neni nejlepsi, protoze s touto kolekci muzeme chtit dale pracovat, objekty v kolekci menit a treba znova ulozit do DB. Ovsem s yield toho nedocilime. (ToList() muzeme samozrejme zavolat vzdy :o)
To se mi líbíTo se mi líbí
Je to úplně to samé jako v případě LINQu – voláním iterátoru dostaneme pouze objekt, který reprezentuje konkrétní dotaz a tudíž jeho opakovaná iterace způsobí opakové vykonání dotazu. A stejně jako v případě LINQu by toto mělo být řádně dokumentováno. Dobrá poznámka.
Je to asi otázka názoru, ale já považuju použití yieldu u GetObjects() za vhodné. Jak píšeš, na Tvůj případ lze moje řešení převést prostým voláním ToList() nebo ToArray(), ale z Tvého řešení se na moje nedostaneš…
Btw. jsem měl původně připraven právě příklad s měněním objektů a následným posílání do DALu k uložení, ale přišlo mi to už moc překombinované 🙂 I s yieldem to ale krásně lze…
To se mi líbíTo se mi líbí
Orion: tady je otázka, jestli člověk chce iterovat, nebo objekty někam ukládat. Osobně častěji potřebuju přes výsledky jenom iterovat a pro každý záznam udělat určitou operaci. V případě, že záznamů jsou desítky tisíc a víc, je ukládání do kolekce zbytečná a neefektivní operace. Naopak v případě, že člověk skutečně potřebuje tu kolekci, je konverze z yieldového IEnumerable do List vcelku efektivní.
Příklad s GetObjects() je problém ještě v trochu jiné věci – co se stane se všemi těmi IDisposable v případě, že uživatel metody neprojde celou vrácenou kolekci (např. zavolá x.GetObjects().First()). Tipoval bych, že zůstanou otevřené, dokud si jich nevšimne GC. A to by třeba na frekventovanějších webech mohlo dělat ošklivé problémy…
To se mi líbíTo se mi líbí
Ondra[sej]: Všechny ty IDisposable se uzavřou, protože implementace Firstu je taková, že zabije pomocí Dispose vrácený Enumerator, čímž dojde i k vykonání finally bloků v yield metodě.
To se mi líbíTo se mi líbí