Programovať asynchrónne však nie je vôbec jednoduché a programátor okrem väčšej komplexnosti čelí aj problémom ako race conditions a pod. Našťastie existujú knižnice, ktorých cieľom je uľahčiť programovanie v asynchrónnom svete. Jednou z takýchto knižníc je Reactive Extensions pre .NET.
Reactive Extensions
Reactive Extensions (ďalej Rx) je knižnica pre .NET, ktorá zavádza trochu iný pohľad na prácu s dátami, najmä kolekciami a udalosťami. Aktuálne je Rx dostupné pre nasledujúce .NET platformy:
- .NET Framework 3.5 SP1
- .NET Framework 4
- Silverlight 4
- Silverlight 5
- Windows Phone 7
- Windows Phone 7.1
Podľa popisu na webe projektu je to „library to compose asynchronous and event-based programs using observable collections and LINQ-style query operators.“, teda knižnica na vytváranie programov založených na asynchrónných operáciách a udalostiach pomocou kolekcií implementujúcich IObservable<T> s plnou podporou LINQ operátorov.
Pre lepšie pochopenie Rx musíme najprv pochopiť pojmy asynchrónne programovanie, LINQ a návrhový vzor Observer.
Asynchrónne programovanie
Pojem asynchrónne programovanie by mal byť jasný každému programátorovi, nezaškodí však malé pripomenutie. Klasický program v imperatívnom programovacom jazyku pozostáva zo sledu príkazov, ktoré sú vykonávané postupne jeden za druhým. Ak teda zavoláme metódu A a potom metódu B, metóda B bude vykonaná až potom, čo metóda A skončí. Metóda B môže využívať výsledok metódy A, môže sa spoľahnúť na to, že v čase svojho spustenia bude dostupný.
Pri asynchrónnom programovaní táto podmienka sekvenčnosti vykonávania príkazov neplatí. Ak je metóda A asynchrónna, jej zavolaním sa rozbehne nové vlákno (proces), ktoré je zodpovedné za jej vykonanie a tok programu pokračuje vykonávaním metódy B. V závislosti na tom, ako dlho vykonanie metódy A trvá, môže byť metóda B vykonaná pred, po, alebo počas vykonávania metódy A a nemôže sa spoliehať na dostupnosť jej výsledku. Programovací jazyk typicky obsahuje možnosť, ako sa o skončení metódy A dozvedieť, najčastejšie ide o udalosti (events).
LINQ
Skratka LINQ znamená Language INtegrated Query a predstavuje rozšírenie .NET (od 3.0) o možnosť dotazovania a transformácie kolekcií, relačných dát a XML dokumentov. LINQ v podstate umožňuje dotazovanie podobné SQL nad dátami bez ohľadu na to, odkiaľ pochádzajú.
Ďalším podstatným znakom LINQ je, že do .NET prináša viac deklaratívny (funkcionálny) prístup k programovaniu. Na rozdiel od klasického imperatívneho programovania sa pri použití LINQ sústredíme na to, čo chceme dosiahnuť a nie na to, ako to dosiahnuť.
Predstavme si, že chceme z nejakej kolekcie vybrať všetky objekty spĺňajúce nejakú podmienku. V imperatívnom prístupe musíme rozmýšľať o tom, ako to dosiahnuť, teda o vytvorení novej kolekcie na uloženie výsledku, prechode prvkov foreach cyklom, teste objektov na danú podmienku a pridávaní prvkov do novej kolekcie.
Kód pre získanie párnych čísiel by mohol vyzerať takto:
List<int> numbers= Enumerable.Range(1,10); List<int> evenNumbers= new List<int>(); foreach (int number in numbers) { if (number % 2 ==0) { evenNumbers.Add(number) } }
Enumerable.Range je funkcia, ktorej zadáme počiatočné číslo a počet čísel a vygeneruje daný interval.
V prípade LINQ a deklaratívneho programovania sa naozaj sústredime len na to, čo chceme vykonať, teda vybrať objekty spĺňajúce podmienku do novej kolekcie
List<int> numbers= Enumerable.Range(1,10); List<int> evenNumbers = (from number in numbers where number %% 2 == 0 select number).ToList();
LINQ umožňuje používať aj alternatívnu syntax, v ktorej sú jednotlivé operátory volané ako funkcie
List<int> numbers= Enumerable.Range(1,10); List<int> evenNumbers = numbers.Where(number => number % 2 == 0).Select(number => number).ToList();
Návrhový vzor Observer
Observer je návrhový vzor, v ktorom si objekt zvaný subjekt uchováva zoznam závislých objektov, tzv. observerov, ktorých informuje o zmenách svojich vlastností, najčastejšie volaním ich metód.
Príkladom dátovej štruktúry implementujúcej návrhový vzor Observer v .NET môže byť ObservableCollection používaná v Silverlighte a Windows Phone 7. Ak túto kolekciu (subjekt) napojíme napríklad na listbox (observer) a pridávame do nej prvky, ObservableCollection automaticky oznámi listboxu, že v nej došlo k zmene (pridanie, odobranie, update prvku) a listbox sa automatický prekreslí tak, aby túto zmenu zohľadnil.
Push vs Pull model
Princíp Rx sa dá asi najlepšie vysvetliť práve na kolekciách. Všetky kolekcie v .NET implementujú rozhranie IEnumerable<T> a dáta z nich „vyťahujeme“. Napríklad pri použití foreach cyklu na kolekciu je najprv nutné získať iterátor pomocou GetEnumerator, následne je volaná metóda MoveNext, ktorá určuje, či sa v kolekcií nachádza ešte nejaký ďalší prvok. Ak áno, je dostupný pomocou vlastnosti Current. Tento model sa nazýva pull model (pull = vyťahovať).
Foreach cyklus z predchádzajúceho príkladu vyzerá v skutočnosti takto:
List<int> numbers= Enumerable.Range(1,10); List<int> evenNumbers= new List<int>(); var e= numbers.GetEnumerator(); while (e.MoveNext()) { int number = e.Current; if (number % 2 ==0) { evenNumbers.Add(number) } }
Dátový model LINQ to Rx je matematický dúalny model k modelu IEnumerable<T>. Namiesto toho, aby sme prvky z kolekcie vyťahovali, kolekcia nám ich sama posiela. Ide o tzv. push model. Základom tohto modelu sú rozhrania IObservable<T> a IObserver<T>, ktoré sú súčasťou .NET 4.0 a sú duálne k IEnumerable<T> a IEnumerator<T>.
S push modelom sa stretávame napríklad pri udalostiach v UI. Klasický button nám posiela udalosť Click pri každom svojom stlačení. Iným príkladom môžu byť asynchrónne volania webových služieb. Jedným z cieľov Rx je práve zjednotiť a hlavne zjednodušiť tieto operácie.
LINQ to Rx
Použitie LINQ to Rx je podobné použitiu udalostí. K IObservable<T> kolekcii je najprv potrebné sa naviazať pomocou metódy Subscribe (obdoba += pri udalostiach). Pomocou rozhrania IObserver<T> sú nám následne vystavené udalosti OnNext, OnCompleted a OnError, na ktoré môžeme reagovať.
Kolekcia nám bude posielať dáta, ktoré môžeme spracovávať po vyvolaní OnNext. Koniec kolekcie je oznámený udalosťou OnCompleted a chyba udalosťou OnError.
Pre lepšiu predstavu uvádzam diagram, ktorý ukazuje, ako funguje foreach v oboch (pull a push) modeloch.
Jednoduchý príklad
Rx implementuje väčšinu LINQ operátorov a podporuje reťazenie, modifikovaný predchádzajúci príklad (vypisuje párne čísla, nevkladá ich do zoznamu) by v push modeli vyzeral takto:
var numbers= Observable.Range(0, 10); numbers.Where(number=> number & 2 == 0).Subscribe( x => Console.WriteLine("OnNext: {0}", x), ex => Console.WriteLine("OnError: {0}", ex.Message), () => Console.WriteLine("OnCompleted"))
Observable.Range je push obdoba Enumerable.Range s rovnakými parametrami.
Použitie ďalších LINQ operátorov spolu s Rx je podobné, napríklad klasický Select použijeme tak, že vytvoríme query a následne sa na danú query naviažeme:
var numbers = Observable.Range(0, 10); var query = from number in numbers where number % 2 == 0& select number * number; query.Subscribe(Console.WriteLine);
Možno vás napadla otázka, kedy vlastne začne kolekcia odosielať dáta. Záleží to od typu kolekcie. Rozlíšujeme tzv. hot a cold obervables. Cold observables začnú posielať dáta ihneď po naviazaní (Subsrcibe), príkladom je použité Range. Hot observables posielajú prvky, aj keď na nich nie je nikto naviazaný, príkladom môžu byť udalosti myši.
Praktický príklad
Doteraz uvedené príklady nemali z praktického hľadiska veľké využitie a pravdepodobne nikoho nepresvedčilo, aby začal Rx používať. Ukážeme si preto praktické použitie vo Windows Phone 7.
Predstavme si nasledujúci scénar. Vo Windows Phone 7 aplikácií máme TextBox, do ktorého používateľ píše text, ktorý chce vyhľadať na Wikipédií. Pod TextBoxom je komponenta WebBrowser, ktorá nájdenú stránku Wikipédie zobrazuje. Vzhľadom na dobrú použiteľnosť chceme výsledky vo Wikipédií zobrazovať nie po každom zadanom znaku ale až v momente, keď používateľ prestane na chvíľu písať.
Bez Rx by bolo potrebné reagovať na každú udalosť KeyUp textboxu, použiť nejaký timer na meranie pauzy medzi týmito udalosťami a kód by bol veľmi komplexný.
V Rx (za predpokladu, že daný TextBox sa volá Search) sa najprv naviažeme na udalosti KeyUp
var keys = Observable.FromEvent<KeyEventArgs>( Search, "KeyUp" )
A použijeme operátor Throttle
var keys = Observable.FromEvent<KeyEventArgs>( Search, "KeyUp" ) .Throttle( TimeSpan.FromSeconds( 0.5 ) );
Kolekcia keys nám teraz bude vďaka Throttle posielať udalosti len v prípade, že používateľ na pol sekundy zastaví písanie. Jednoduché, elegantné a hlavne dobre čitateľné.
Ďalšou dôležitou vlastnosťou okrem toho, že operátory je možné ľubovoľne spájať a kombinovať je fakt, že Observables sú tzv. first class citizens. Znamená to výrazne univerzálnejšie a jednoduchšie použitie, pretože
- Môžu byť uložené v premenných a dátových štruktúrach
- Môžu byť použité ako parametre metód
- Možu byť použité ako návratové hodnoty metód
- Môžu byť vytvorené za behu
- Majú skutočnú hodnotu nezávislú na názve
Záver
Cieľom tohto článku bolo oboznámenie sa s Rx, push modelom a ukážka praktického použitia. Rx je zaujímavý hlavne v tom, že núti programátora uvažovať v trochu inej rovine, ako bol doteraz zvyknutý Je to však silný nástroj, ktorého pochopením a používaním je možné zjednodušiť si prácu s asynchrónnymi operáciami.
Přehled komentářů