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); } }
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.
Nie zmienia to faktu że wiedza przydatna i że jestem wdzięczny za nią. 🙂
Czy w ostatnim przykładzie rozszerzenia FirstNameChanged.RaiseEvent(this,e); nie może rzucić NullReferenceExcetpion ?
@Grzgorz:
Nie ponieważ jest to statyczna metoda. Zobacz tutaj:
http://www.pzielinski.com/?p=1371