Úvod do Reactive Extensions

Reactive Extensions

Asynchrónne programovanie sa v súčasnosti dostáva stále viac do pozornosti, pomaly sa stáva nutnosťou a budeme sa s ním stretávať stále častejšie. Napríklad vo Windows Phone 7 je dobrým zvykom vykonávať náročné operácie asynchrónne v osobitnom vlákne, aby UI aj naďalej reagovalo na vstup používateľa, vo Windows 8 je dokonca nutné všetky operácie, ktoré môžu trvať dlhšie ako 50 milisekúnd, vykonávať asynchrónne.

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ť ObservableCollec­tion 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, ObservableCollec­tion 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 tak­to:

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.

Obrázok 1: Sekvenčný diagram pull a push modelu (prebrané z knihy C# in depth od Joa Skeeta)

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.

Autor je absolventom softvérového inžinierstva na Univerzite Karlovej v Prahe, pracuje ako Windows Azure a Windows Phone vývojár v Inmite, občas publikuje a prednáša.

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 15

Přehled komentářů

blizz Re: Úvod do Reactive Extensions
Aleš Roubíček Re: Úvod do Reactive Extensions
blizz Re: Úvod do Reactive Extensions
Friv Re: Úvod do Reactive Extensions
Rene Stein Pár poznámek k RX
backup windows 8 , 50 milisekund
Martin Hassman Re: windows 8 , 50 milisekund
Rene Stein Re: windows 8 , 50 milisekund
Rene Stein Re: windows 8 , 50 milisekund
Martin Hassman Re: windows 8 , 50 milisekund
Rene Stein Re: windows 8 , 50 milisekund
Rene Stein Re: windows 8 , 50 milisekund
Martin Hassman Re: windows 8 , 50 milisekund
igorkulman Re: windows 8 , 50 milisekund
msx Re: windows 8 , 50 milisekund
Zdroj: https://www.zdrojak.cz/?p=3649