Widziałem kilka wzmianek o tym idiomie (w tym na SO ):
// Deliberately empty subscriber
public event EventHandler AskQuestion = delegate {};
Zaleta jest oczywista - pozwala uniknąć konieczności sprawdzania wartości null przed podniesieniem zdarzenia.
Chciałbym jednak zrozumieć, czy są jakieś wady. Na przykład, czy jest to coś, co jest w powszechnym użyciu i jest na tyle przezroczyste, że nie spowoduje bólu głowy związanego z konserwacją? Czy jest jakieś znaczące uderzenie w wydajność pustego połączenia abonenta zdarzenia?
Zamiast wywoływać narzut wydajności, dlaczego nie skorzystać z metody rozszerzającej, aby złagodzić oba problemy:
public static void Raise(this EventHandler handler, object sender, EventArgs e) { if(handler != null) { handler(sender, e); } }
Po zdefiniowaniu nigdy nie musisz ponownie sprawdzać zdarzenia zerowego:
// Works, even for null events. MyButtonClick.Raise(this, EventArgs.Empty);
źródło
handler
powyższy parametr jest parametrem wartości metody. Nie zmieni się, gdy metoda jest uruchomiona. Aby tak się stało, musiałby to byćref
parametr (oczywiście nie jest dozwolone, aby parametr miał zarówno atrybut, jakref
ithis
modyfikator) lub oczywiście pole.W przypadku systemów, które intensywnie wykorzystują zdarzenia i mają krytyczne znaczenie dla wydajności , na pewno warto przynajmniej rozważyć zaniechanie tego. Koszt podniesienia zdarzenia z pustym delegatem jest w przybliżeniu dwa razy większy niż koszt podniesienia go za pomocą sprawdzenia wartości null.
Oto kilka liczb z testami porównawczymi na moim komputerze:
For 50000000 iterations . . . No null check (empty delegate attached): 530ms With null check (no delegates attached): 249ms With null check (with delegate attached): 452ms
A oto kod, którego użyłem do uzyskania tych liczb:
using System; using System.Diagnostics; namespace ConsoleApplication1 { class Program { public event EventHandler<EventArgs> EventWithDelegate = delegate { }; public event EventHandler<EventArgs> EventWithoutDelegate; static void Main(string[] args) { //warm up new Program().DoTimings(false); //do it for real new Program().DoTimings(true); Console.WriteLine("Done"); Console.ReadKey(); } private void DoTimings(bool output) { const int iterations = 50000000; if (output) { Console.WriteLine("For {0} iterations . . .", iterations); } //with anonymous delegate attached to avoid null checks var stopWatch = Stopwatch.StartNew(); for (var i = 0; i < iterations; ++i) { RaiseWithAnonDelegate(); } stopWatch.Stop(); if (output) { Console.WriteLine("No null check (empty delegate attached): {0}ms", stopWatch.ElapsedMilliseconds); } //without any delegates attached (null check required) stopWatch = Stopwatch.StartNew(); for (var i = 0; i < iterations; ++i) { RaiseWithoutAnonDelegate(); } stopWatch.Stop(); if (output) { Console.WriteLine("With null check (no delegates attached): {0}ms", stopWatch.ElapsedMilliseconds); } //attach delegate EventWithoutDelegate += delegate { }; //with delegate attached (null check still performed) stopWatch = Stopwatch.StartNew(); for (var i = 0; i < iterations; ++i) { RaiseWithoutAnonDelegate(); } stopWatch.Stop(); if (output) { Console.WriteLine("With null check (with delegate attached): {0}ms", stopWatch.ElapsedMilliseconds); } } private void RaiseWithAnonDelegate() { EventWithDelegate(this, EventArgs.Empty); } private void RaiseWithoutAnonDelegate() { var handler = EventWithoutDelegate; if (handler != null) { handler(this, EventArgs.Empty); } } } }
źródło
Jeśli robisz to często / dużo /, możesz chcieć mieć pojedynczego, statycznego / udostępnionego pustego delegata, którego ponownie użyjesz, po prostu w celu zmniejszenia liczby wystąpień delegatów. Zauważ, że kompilator i tak zapisuje tego delegata w pamięci podręcznej dla każdego zdarzenia (w polu statycznym), więc jest to tylko jedno wystąpienie delegata na definicję zdarzenia, więc nie jest to duża oszczędność - ale może warto.
Oczywiście pole na instancję w każdej klasie będzie nadal zajmowało to samo miejsce.
to znaczy
internal static class Foo { internal static readonly EventHandler EmptyEvent = delegate { }; } public class Bar { public event EventHandler SomeEvent = Foo.EmptyEvent; }
Poza tym wydaje się w porządku.
źródło
Nie ma znaczącej utraty wydajności, o której można by mówić, z wyjątkiem, być może, niektórych ekstremalnych sytuacji.
Należy jednak pamiętać, że ta sztuczka staje się mniej istotna w C # 6.0, ponieważ język zapewnia alternatywną składnię do wywoływania delegatów, które mogą mieć wartość null:
delegateThatCouldBeNull?.Invoke(this, value);
Powyżej, operator warunkowy o wartości null
?.
łączy sprawdzanie wartości null z wywołaniem warunkowym.źródło
Rozumiem, że pusty delegat jest bezpieczny dla wątków, podczas gdy sprawdzenie wartości null nie.
źródło
Powiedziałbym, że to trochę niebezpieczny konstrukt, ponieważ kusi, aby zrobić coś takiego:
MyEvent(this, EventArgs.Empty);
Jeśli klient zgłosi wyjątek, serwer idzie z nim.
Więc może tak:
try { MyEvent(this, EventArgs.Empty); } catch { }
Ale jeśli masz wielu subskrybentów i jeden subskrybent zgłasza wyjątek, co dzieje się z pozostałymi subskrybentami?
W tym celu używam statycznych metod pomocniczych, które sprawdzają wartość null i połykają każdy wyjątek po stronie subskrybenta (to jest z idesign).
// Usage EventHelper.Fire(MyEvent, this, EventArgs.Empty); public static void Fire(EventHandler del, object sender, EventArgs e) { UnsafeFire(del, sender, e); } private static void UnsafeFire(Delegate del, params object[] args) { if (del == null) { return; } Delegate[] delegates = del.GetInvocationList(); foreach (Delegate sink in delegates) { try { sink.DynamicInvoke(args); } catch { } } }
źródło
Try.TryAction()
funkcji, a nie jawnegotry{}
bloku), ale nie ignoruję wyjątków,Zamiast podejścia „pustego delegata” można zdefiniować prostą metodę rozszerzenia, aby hermetyzować konwencjonalną metodę sprawdzania obsługi zdarzeń pod kątem wartości null. Opisane jest tutaj i tutaj .
źródło
Jak dotąd jedna rzecz jest pomijana jako odpowiedź na to pytanie: unikanie sprawdzania wartości zerowej jest niebezpieczne .
public class X { public delegate void MyDelegate(); public MyDelegate MyFunnyCallback = delegate() { } public void DoSomething() { MyFunnyCallback(); } } X x = new X(); x.MyFunnyCallback = delegate() { Console.WriteLine("Howdie"); } x.DoSomething(); // works fine // .. re-init x x.MyFunnyCallback = null; // .. continue x.DoSomething(); // crashes with an exception
Rzecz w tym, że nigdy nie wiadomo, kto użyje Twojego kodu w jaki sposób. Nigdy nie wiadomo, czy w ciągu kilku lat podczas naprawy błędu w kodzie zdarzenie / obsługa zostanie ustawiona na null.
Zawsze wypisz czek if.
Mam nadzieję, że to pomoże;)
ps: Dzięki za obliczenia wydajności.
pps: Edytowano go z przypadku zdarzenia do i przykład wywołania zwrotnego. Dzięki za informację zwrotną ... „Zakodowałem” przykład bez programu Visual Studio i dostosowałem przykład, który miałem na myśli, do zdarzenia. Przepraszam za zamieszanie.
ppps: Nie wiem, czy nadal pasuje do wątku… ale myślę, że to ważna zasada. Sprawdź także inny wątek stackflow
źródło
protected
. Możesz przerwać zdarzenie tylko z samej klasy (lub z klasy pochodnej).