Czy istnieje wada dodawania anonimowego pustego delegata w deklaracji zdarzenia?


82

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?

Odpowiedzi:


36

Jedynym minusem jest bardzo niewielki spadek wydajności, ponieważ wzywasz dodatkowego pustego delegata. Poza tym nie ma kary za utrzymanie ani innej wady.


19
Jeśli w przeciwnym razie zdarzenie miałoby dokładnie jednego subskrybenta ( bardzo częsty przypadek), fikcyjna obsługa sprawi, że będzie miało dwóch. Zdarzenia z jednym programem obsługi są obsługiwane znacznie wydajniej niż te z dwoma.
supercat

46

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);

2
Zobacz tutaj, aby
zapoznać się

1
Dokładnie to, co robi moja pomocnicza biblioteka trójcy, nawiasem mówiąc: thehelpertrinity.codeplex.com
Kent Boogaart

3
Czy to nie tylko przenosi problem wątku sprawdzania wartości null do metody rozszerzenia?
PatrickV

6
Nie. Procedura obsługi jest przekazywana do metody i wtedy nie można zmodyfikować tej instancji.
Judah Gabriel Himango

@PatrickV Nie, Judah ma rację, handlerpowyższy parametr jest parametrem wartości metody. Nie zmieni się, gdy metoda jest uruchomiona. Aby tak się stało, musiałby to być refparametr (oczywiście nie jest dozwolone, aby parametr miał zarówno atrybut, jak refi thismodyfikator) lub oczywiście pole.
Jeppe Stig Nielsen

42

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);
            }
        }
    }
}

11
Żartujesz, prawda? Inwokacja dodaje 5 nanosekund i ostrzegasz przed tym? Nie mogę wymyślić bardziej nierozsądnej ogólnej optymalizacji niż to.
Brad Wilson,

8
Ciekawy. Zgodnie z ustaleniami, szybsze jest sprawdzenie wartości null i wezwanie delegata, niż tylko wywołanie go bez sprawdzenia. Nie brzmi to dla mnie dobrze. W każdym razie jest to tak mała różnica, że ​​nie sądzę, aby była zauważalna we wszystkich, z wyjątkiem skrajnych przypadków.
Maurice,

22
Brad, powiedziałem szczególnie dla systemów krytycznych dla wydajności, które intensywnie wykorzystują zdarzenia. Jak to ogólnie?
Kent Boogaart

3
Mówisz o karach za wydajność, dzwoniąc do pustych delegatów. Pytam: co się stanie, gdy ktoś faktycznie zapisze się na wydarzenie? Powinieneś martwić się o wydajność subskrybentów, a nie pustych delegatów.
Liviu Trifoi

2
Duży koszt wydajności nie występuje w przypadku, gdy nie ma „prawdziwych” abonentów, ale w przypadku, gdy jest jeden. W takim przypadku subskrypcja, anulowanie subskrypcji i wywołanie powinny być bardzo wydajne, ponieważ nie będą musiały nic robić z listami wywołań, ale fakt, że zdarzenie będzie (z punktu widzenia systemu) mieć dwóch subskrybentów, a nie jednego, wymusi użycie kodu obsługującego „n” subskrybentów, a nie kodu zoptymalizowanego dla przypadku „zero” lub „jeden”.
supercat

7

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.


1
Z odpowiedzi tutaj: stackoverflow.com/questions/703014/, wynika, że ​​kompilator przeprowadza już optymalizację dla jednej instancji.
Govert

3

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.



2

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
        { }
    }
}

Nie po to, żeby szukać, ale nie myśl <code> if (del == null) {return; } Delegate [] delegates = del.GetInvocationList (); </code> czy jest kandydatem do wyścigu?
Cherian

Nie do końca. Ponieważ delegaci są typami wartości, del jest w rzeczywistości prywatną kopią łańcucha delegatów, która jest dostępna tylko dla treści metody UnsafeFire. (Zastrzeżenie: To nie działa, jeśli UnsafeFire dostaje inlined, więc trzeba by użyć [MethodImpl (MethodImplOptions.NoInlining)] atrybut przyzwyczaić przeciwko niemu.)
Daniel Fortunov

1) Delegaci są typami referencyjnymi 2) Są niezmienne, więc nie jest to stan wyścigu 3) Nie sądzę, aby wstawianie inlining zmieniało zachowanie tego kodu. Spodziewałbym się, że parametr del stanie się nową zmienną lokalną po wstawieniu.
CodesInChaos

Twoim rozwiązaniem „co się stanie, jeśli zostanie zgłoszony wyjątek” jest zignorowanie wszystkich wyjątków? Dlatego zawsze lub prawie zawsze zawijam wszystkie subskrypcje zdarzeń próbą (używając przydatnej Try.TryAction()funkcji, a nie jawnego try{}bloku), ale nie ignoruję wyjątków,
zgłaszam

0

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 .


-2

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


1
x.MyFunnyEvent = null; <- To nawet się nie kompiluje (poza klasą). Celem zdarzenia jest tylko wspieranie + = i - =. Nie możesz nawet zrobić x.MyFunnyEvent- = x.MyFunnyEvent poza klasą, ponieważ funkcja pobierająca zdarzenie jest quasi protected. Możesz przerwać zdarzenie tylko z samej klasy (lub z klasy pochodnej).
CodesInChaos

masz rację ... prawda o wydarzeniach ... miałeś sprawę z prostą obsługą. Przepraszam. Spróbuję edytować.
Thomas

Oczywiście, jeśli Twój pełnomocnik jest publiczny, byłoby to niebezpieczne, ponieważ nigdy nie wiesz, co zrobi użytkownik. Jeśli jednak ustawisz pustych delegatów w zmiennej prywatnej i sam obsłużysz + = i - =, nie będzie to problem, a sprawdzenie wartości null będzie bezpieczne dla wątków.
ForceMagic,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.