Wywoływanie zdarzeń za pomocą metod rozszerzających

O zdarzeniach było już wielokrotnie na blogu. Pokazywałem różne sposoby wywołania zdarzeń. Najpopularniejszym chyba sposobem jest poniższy wzorzec:

public class Person
{
   public event EventHandler FirstNameChanged;
   
   virtual protected void OnFirstNameChanged(EventArgs e)        
   {
       if (FirstNameChanged != null)
           FirstNameChanged(this, e);            
   }
}

Jeśli wielowątkowość wchodzi w grę wtedy lepiej napisać:

public class Person
{
   public event EventHandler FirstNameChanged;
   
   virtual protected void OnFirstNameChanged(EventArgs e)
   {
       EventHandler handler = FirstNameChanged;
       if (handler != null)
           handler(this, e);            
   }
}

Poprzednia instrukcja IF oczywiście była niebezpieczna z punktu widzenia wielowątkowości. W momencie wykonania pierwszego if’a referencja do FirstNameChanged mogłaby zostać ustawiona na NULL tym samym powodując NullReferenceException w momencie wywołania handler’a. Drugi fragment w praktyce działa ale z punktu widzenia wielowątkowości również nie jest bezpieczny. Po pierwsze kompilator mógłby to zoptymalizować i usunąć zmienną tymczasową handler (tym samym niczym by to się nie różniło od pierwszego przykładu). Drugim powodem jest caching, który może spowodować, że nie będzie brana wartość najnowsza a stara z bufora. Z tego co wiem, drugi przykład jest na tyle popularnym wzorcem, że kompilator C# wie o tym i nie będzie próbował zoptymalizować kodu a tym samym spowodować, że kod może zakończyć się NullReferenceException. Z tego względu powszechnie uznaje się, że kod jest bezpieczny. Jeśli jednak ktoś chce pisać kod, który z punktu matematycznego jest poprawny wtedy należy użyć np. Interlocked.Exchange:

public class Person
{
   public event EventHandler FirstNameChanged;

   protected virtual void OnFirstNameChanged(EventArgs e)
   {
       EventHandler handler = Interlocked.CompareExchange(ref FirstNameChanged, null, null);

       if (handler != null)
           handler(this, e);
   }
}

Trzeba przyznać, że jest to dość czasochłonne i nudne – za każdym razem trzeba pisać instrukcję IF. Z tego względu warto napisać metodę rozszerzająca dla EventArgs:

public static class EventArgsExtensions
{
   public static void Raise<TEventArgs>(this TEventArgs e,
                                        Object sender, ref EventHandler<TEventArgs> eventHandler)
       where TEventArgs : EventArgs
   {
       EventHandler<TEventArgs> handler = Interlocked.CompareExchange(ref eventHandler, null, null);
       if (handler != null) 
           handler(sender, e);
   }
}

Teraz w OnFirstNameChanged wystarczy:

public class Person
{
   public event EventHandler<EventArgs> FirstNameChanged;

   protected virtual void OnFirstNameChanged(EventArgs e)
   {
       e.Raise(this,ref FirstNameChanged);
   }
}

Innym ciekawym sposobem jest rozszerzenie samego handler’a”:

static public class EventExtensions
{
   static public void RaiseEvent(this EventHandler eventHandler, object sender, EventArgs e)
   {
       var handler = eventHandler;
       if (handler != null)
           handler(sender, e);
   }
   static public void RaiseEvent<T>(this EventHandler<T> eventHandler, object sender, T e)
       where T : EventArgs
   {
       var handler = eventHandler;
       if (handler != null)
           handler(sender, e);
   }
}

W takim przypadku wywołanie FirstNameChanged sprowadza się do:

public class Person
{
   public event EventHandler<EventArgs> FirstNameChanged;

   protected virtual void OnFirstNameChanged(EventArgs e)
   {
       FirstNameChanged.RaiseEvent(this,e);
   }
}

4 thoughts on “Wywoływanie zdarzeń za pomocą metod rozszerzających”

  1. IMO ten kod jest nadmiarowy.
    Żeby to miało głębszy sens należałoby ukryć extensiona w jakimś namespacie z ThreadSafe w nazwie. Albo samą funkcję wywołującą zdarzenie jakoś oznaczyć że to “nadmiarowa, ale za to threadsafe” wersja.

  2. Czy w ostatnim przykładzie rozszerzenia FirstNameChanged.RaiseEvent(this,e); nie może rzucić NullReferenceExcetpion ?

Leave a Reply

Your email address will not be published.