Občas není na škodu být lenivý, mnohdy se to vyplatí! To platí
i o našich programech, zejména o těch, které zpracovávají veliké
množství dat. Jistě si říkáte, jak může být můj program lenivý, já
přeci chci, aby byl co nejvýkonnější. Ano, to se nevylučuje, právě
naopak! :)
Když jsem nedávno psal o tom, že Vracet list je špatné,
neshledal jsem se s velikým porozuměním. Tento článek byl o návrhu
rozhraní našich tříd. Dneska se podíváme na implementační detaily a
výhody plynoucí z vracení (zvracení?) IEnumerable<>.
LINQ2SQL a metoda ToList()
Začněme tedy u nevýhod užívání generického listu v LINQu. Vsadím
se, že většina z vás pro vykonání LINQového dotazu do databáze zavolá
metodu ToList(). Okamžitě se tím vykoná dotaz a jeho výsledek
je natažen do paměti, konkrétně jako položky generického listu. Jistě se
tím zvýší výkon při iteraci přes výslednou kolekci. To je super, ale pro
pro výpis používat velice drahý obal v podobě listu? List umožňuje
kolekce snadno modifikovat, to je jistě skvělá vlastnost, ale také sebou
nese zbytečně alokovanou paměť navíc.
Mnohem výhodnější je volat metodu ToArray(), která vykoná
dotaz úplně stejným způsobem, ale vezme si jen tolik paměti, kolik opravdu
potřebuje, nehledě na to, že samotný .net runtime je na práci s
Array optimalizovaný. Jednoduchou změnou můžeme ušetřit
spoustu paměti a zlepšit tak výkon naší aplikace. Pokud jsme však do
našich rozhraní zanesli IList<>, musíme teď měnit
spoustu kódu, protože náš původní návrh byl krátkozraký.
Zpátky k lenivosti
To, co jsme si popsali v předchozích řádcích, nemá s lenivostí
stále nic společného, jde jen o malou optimalizaci dychtivého přístupu
k datům (eager loading). Ten samozřejmě nemusí být z principu špatný,
ale hodí se pouze pro data omezeného rozsahu. Pokud zpracováváme velké nebo
předem neznámé množství dat je lepší zlenivět. Tím se opět vracíme k
IEnumerable<>.
Vezměme si hypotetický příklad že náš program bude zpracovávat
nekonečný zdroj pravdy:
static IEnumerable<Boolean> GetInfinityTruth() {
while (true) {
yield return true;
}
}
Fůj, napsal jsem nekonečnou smyčku. Hele, ale jak jinak byste chtěli
udělat nekonečný zdroj pravdy? Pomocí listu? Těžko. ;) Takovýhle kód je
zcela validní a bezproblémový. Tedy do doby, než se najde někdo šikovný,
komu se hodí list, a nad GetInfinityTruth() zavolá
ToList(). Asi nám vyskočí
OutOfMemoryException. :)
Vážně, výhodou tohoto kódu je, že (teoreticky) vrací nekonečné
množství dat a přesto se s ním dá v klidu pracovat, aniž by nám
sežralo adekvátní množství paměti (opět nekonečné). A to díky
lenivosti! S takovýmto přístupem se nám totiž vyhodnocuje vždy jen jeden
prvek z kolekce a to až v případě, že je opravdu potřeba. Oddalujeme
načítání prvku do doby, kdy s ním opravdu budeme něco dělat.
Další výhodou je, že zcela bez problémů, můžeme celou tu nekonečnou
pravdu popřít:
static IEnumerable<Boolean> Disclaim(this IEnumerable<Boolean> source) {
return source.Select(value => !value);
}
Samozřejmě bych to mohl napsat i takhle:
static IEnumerable<Boolean> Disclaim(this IEnumerable<Boolean> source) {
foreach(bool value in source) {
yield return !value;
}
}
Ale to je příliš mnoho psaní. Nepište zbytečný kód!
Teďka, když zavolám GetInfinityTruth().Disclaim(), dostanu
nekonečný zdroj nepravdy, tedy samé lži. :) Opět to funguje tak, že je
popřen každý konkrétní prvek, až když na něj dojde, ne všechny
najednou, to by bylo moc práce. On se totiž příjemce naší nekonečné
pravdy může kdykoli rozhodnout, že už ho to nebaví. A co my pak s tím,
že jsme mu dopředu připravili krásné lži? Nic. Byla to zbytečná práce.
V tomto případě se lenivost vyplatila.
Závěr
„Poslouchaj Roubíček, oni jsou nejspíš blázen. Nás tu krměj
o nekonečné pravdě, co my s tím?“ No, je to určitá abstrakce. ;)
Související