Przypuśćmy że mamy zdarzenie
public event EventHandler<EventArgs> Bang;
Aby je wywołać, wystarczy napisać Bang(this, EventArgs.Empty);
Nie… Jeśli do zdarzenia nie jest podpięta żadna procedura obsługi tegoż, otrzymamy NullReferenceExcepion.
A zatem if (Bang != null) Bang(this, EventArgs.Empty);
Nie… Taka konstrukcja będzie się znakomicie sprawdzała tak długo, jak długo nasza aplikacja będzie jednowątkowa. Jeśli wątków będzie wiecej, może zdarzyć się sytuacja, kiedy jeden wątek będzie próbował wywołać zdarzenie a drugi opróżniał jego listę inwokacji. Szeregowaniem wątków i procesów zajmuje się system operacyjny i nie mamy wpływu na to, kiedy nastąpi przełączenie kontekstu. Jak będziemy mieli pecha, instrukcja czyszcząca listę inwokacji zdarzenia zostanie czasowo wciśnięta pomiędzy if (Bang != null)
a Bang(...)
. I znów będzie wyjątek.
A zatem:
1 2 |
EventHandler<EventArgs> temp = Bang; if (temp != null) temp(this, EventArgs.Empty); |
W większości przypadków, to już wystarczy. Ale…
Co się stanie, jeżeli jedna z procedur obsługi zdarzenia wyrzuci wyjątek? W którym miejscu powinien on być obsłużony i czy powinno mieć to wpływ na wykonanie pozostałych procedur obsługi? W powyższym przykładzie wystapienie wyjątku spowoduje, że procedury znajdujące się na liście inwokacji za procedurą zgłaszającą wyjątek – nie wykonają się w ogóle.
Więc może:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
EventHandler<EventArgs> temp = Bang; if (temp != null) { var list = ev.GetInvocationList(); foreach (EventHandler<EventArgs> d in list) { try { d.Invoke(this, EventArgs.Empty); } catch (Exception) { } } } |
Stąd już o krok od pomysłu, aby wykonywać procedury obsługi zdarzenia współbieżnie (asynchronicznie). Wystarczy zmienić d.Invoke()
na d.BeginInvoke()
. Czy to ma sens? W przypadku, kiedy mowa o zdarzeniach związanych z interfejsem użytkownika – na pewno nie. W innych przypadkach – może warto się zastanowić. Ma to na pewno jedną zaletę: tak jak foreach i try zapewnią wykonanie całej listy inwokacji pomimo wystąpienia wyjątku, tak wywołanie asynchroniczne zapewni wykonanie całej listy inwokacji pomimo zawieszenia się którejś z procedur.
W przypadkach, kiedy wykonujesz własne, solidnie napisane prodecury – zabezpieczanie się blokiem try powinno być zbędne. W przypadku, kiedy do twoich zdarzeń mogą się podpinać procedury z jakichś niezaufanych wtyczek – pomoże zapobiec sytuacji, w której awaria jednej wtyczki wywala całą aplikację.
Tymczasem z prostego Bang() zrobiła się całkiem skomplikowana procedura. A przecież zdarzenia miały być takie proste, wygodne… Spróbujmy to nieco poprawić, tworząc metodę rozszerzającą.
1 2 3 4 5 6 7 8 9 10 11 |
public static class Extensions { public static void Raise<T>(this EventHandler<T> ev, object sender, T e) where T : EventArgs { if (ev != null) { //w tym miejscu możesz też wkleić wersję z foreach i try, jeśli chcesz ev(sender, e); } } } |
Efekt? Zamiast Bang(this, EventArgs.Empty);
, trzeba napisać Bang.Raise(this, EventArgs.Empty);
.
Jeszcze tylko 2 uwagi, które rozwieją wątpliwości, jakie mogą u niektórych się pojawić:
Instrukcja Bang.Raise
zadziała nawet kiedy Bang==null
, ponieważ Raise
jest metodą statyczną, która przyjmuje Bang
jako pierwszy parametr, a jedynie wygląda, jakby była metodą instancji Bang
(o to właśnie chodzi w metodach rozszerzających).
W samym ciele metody zniknęła instrukcja tworząca kopię zdarzenia w zmiennej temp. Teraz operacja ta ukryta jest w przekazaniu zdarzenia jako parametr metody Raise
.
Zamiast tyle się pocić polecam utworzenie metody ‘pustej’ i przypisanie jej do delegata w klasie publikującej zdarzenie. Tak utworzone przypisanie powoduje 100% poprawne funkcjonowanie w trybie wielowątkowym i co najlepsze zdejmuje z nas konieczność nerwowego przejmowania się, czy delegat jest null.
Proste, więc po co się męczyć…
Przecież ten artykuł porusza więcej problemów, niż tylko opróżnienie listy inkowacji, a pokazaną metodę Raise trudno nazwać męczeniem się.