Reactive Extensions: Zdarzenia w RX

Opisywałem już mechanizm konwersji zdarzeń .NET do IObservable. W RX istnieje dodatkowo nowy mechanizm, mający na celu zastąpić standardowe zdarzenia  .NET – przynajmniej w części przypadków. Zastanówmy się, co jest złego tak naprawdę w obsłudze zdarzeń w .NET?

1. Dość często programiści zapominają usunąć zdarzenie co może skutkować memory leak. Czasami jest ciężko zdefiniować moment, w którym należy usunąć zdarzenie. Pewnym rozwiązaniem problemu może być zastosowanie wzorca weak event pattern ale jak wiemy z archiwalnych wpisów, nie jest to też takie proste.

2. Zdarzenia, które zawierają anonimowe metody są szczególnie trudne do usunięcia. Na przykład:

internal class Program
{
   public static event EventHandler MessageReceived;
   private static void Main(string[] args)
   {
       MessageReceived += (sender,e) => Console.WriteLine(e.ToString());
   }
}

W jaki sposób usunąć powyższe zdarzenie? Niestety trzeba stworzyć tymczasową zmienną i ją przechowywać gdzieś:

public static event EventHandler MessageReceived;

private static void Main(string[] args)
{
  EventHandler action=(sender, e) => Console.WriteLine(e.ToString());

  MessageReceived += action;
  // disposing
  MessageReceived -= action;
}

3.  Jak już wspomniałem i pokazałem w jednym z poprzednich wpisów, nie ma możliwości odczytania poprzednich wartości, jakie zostały wygenerowane przez zdarzenie (np. poprzednia pozycja myszki).

4. Zdarzenia to właściwości a nie obiekty co czasami może być niewygodne.

5. Z punktu pisania testów jednostkowych, zdarzenia również nie są szczególnie łatwe do obsługi.

6. Niestety zdarzeń nie można traktować jako kolekcji, z tego względu nie ma możliwości wykorzystania zapytań  LINQ.

Dość narzekania, czas pokazać mechanizm jaki dostarcza RX. Mam nadzieję, że wszyscy są już obeznani z wzorcem projektowym Observer. Jeśli nie, polecam poczytanie o nim( np. na tym blogu, kiedyś o tym już pisałem). Znając zasady działania wzorca obserwator, mechanizm działania zdarzeń RX nie powinien zaskakiwać. Najważniejszym interfejsem jest ISubject, który wygląda następująco:

public interface ISubject<in TSource, out TResult> 
: IObserver<TSource>, IObservable<TResult>
{ 
}
public interface ISubject<T>  : ISubject<T, T>
{ 
}

Proszę zauważyć, że implementuje on zarówno IObserver jak IObservable. W praktyce oznacza to, że klasy implementujące ISubject, mogą zarówno publikować jak i dokonywać subskrypcji (nasłuchiwać). Przykład:

internal class Program
{
   public static  Subject<string> MessageReceived=new Subject<string>();

   private static void Main(string[] args)
   {            
       MessageReceived.Subscribe(Console.WriteLine);
       MessageReceived.OnNext("Hello World");
       Console.ReadLine();
   }
}

W zwykłych zdarzeniach, operator += służy do podłączenia metod. W RX analogiczną funkcję pełni metoda Subscribe. Zasada działania jest taka sama, jak w przypadku dobrze znanego IObservable ( w końcu ISubject implementuje IObservable). W celu wywołania zdarzenia, korzystamy analogicznie z OnNext. Dzięki RX, łatwo zwolnić zdarzenie:

internal class Program
{
   public static  Subject<string> MessageReceived=new Subject<string>();

   private static void Main(string[] args)
   {            
       IDisposable disposable=MessageReceived.Subscribe(Console.WriteLine);
       MessageReceived.OnNext("Hello World");
       disposable.Dispose();
       MessageReceived.OnNext("You will not see this text");

       Console.ReadLine();
   }
}

Oczywiście, można (a nawet jest to polecane) użyć klauzuli using, dla powyższego przykładu.

internal class Program
{
   public static  Subject<string> MessageReceived=new Subject<string>();

   private static void Main(string[] args)
   {            
       using(MessageReceived.Subscribe(Console.WriteLine))
       {
           MessageReceived.OnNext("Hello World");
       }

       MessageReceived.OnNext("You will not see this text");

       Console.ReadLine();
}

Zdarzenie automatycznie zostanie zwolnione (a wszelkie metody połączonego do niej, usunięte) kiedy zmienna nie ma żadnych referencji. Przypomina to trochę weak event pattern, którego cel był analogiczny – ale wtedy należało wykonać dużo więcej roboty. Dzięki RX mamy możliwość większej kontroli nad przepływem zdarzeń za pomocą metod OnNext, OnError, OnCompleted:

internal class Program
{
   public static Subject<string> MessageReceived = new Subject<string>();

   private static void Main(string[] args)
   {
       using (MessageReceived.Subscribe(OnNext,OnCompleted,OnError))
       {
           MessageReceived.OnNext("Processing...");
           try
           {
               // do sth
           }
           catch (Exception e)
           {
               MessageReceived.OnError(e);
           }
           finally
           {
               MessageReceived.OnCompleted();
           }
       }

       Console.ReadLine();
   }
   private static void OnNext(string obj)
   {
   }
   private static void OnCompleted(Exception error)
   {
   }
   private static void OnError()
   {
   }
}

W przypadku standardowych zdarzeń, nie ma oczywiście takiej możliwości. Jeśli ktoś jest zaznajomiony z RX to Subject wygląda bardzo prosto – implementuje on interfejsy, które były bardzo dokładnie omówione w poprzednich wpisach.

Leave a Reply

Your email address will not be published.