Чи є недолік додавання анонімного порожнього делегата в оголошенні події?


82

Я бачив кілька згадувань про цю ідіому (в тому числі про SO ):

// Deliberately empty subscriber
public event EventHandler AskQuestion = delegate {};

Переваги зрозумілі - це дозволяє уникнути необхідності перевіряти наявність нуля перед тим, як підняти подію.

Однак я хочу зрозуміти, чи є якісь мінуси. Наприклад, чи є це щось широко поширене і є достатньо прозорим, щоб не викликати головного болю в обслуговуванні? Чи є якийсь помітний показник продуктивності дзвінка абонента порожньої події?

Відповіді:


36

Єдиний мінус - дуже незначне покарання за продуктивність, оскільки ви викликаєте зайвого порожнього делегата. Крім цього, не передбачено штрафу за обслуговування або інших недоліків.


19
Якщо в іншому випадку подія мала б рівно одного абонента ( дуже поширений випадок), фіктивний обробник зробить його двома. Події з одним обробником обробляються набагато ефективніше, ніж з двома.
supercat

46

Замість того, щоб викликати накладні витрати на продуктивність, чому б не використати метод розширення для полегшення обох проблем:

public static void Raise(this EventHandler handler, object sender, EventArgs e)
{
    if(handler != null)
    {
        handler(sender, e);
    }
}

Одного разу визначившись, вам більше ніколи не доведеться робити ще одну перевірку нульової події:

// Works, even for null events.
MyButtonClick.Raise(this, EventArgs.Empty);

2
Дивіться тут для загальної версії: stackoverflow.com/questions/192980 / ...
Benjol

1
Саме те, що робить моя допоміжна бібліотека трійці, до речі: thehelpertrinity.codeplex.com
Кент Бугаарт

3
Чи це не просто переміщує проблему потоку нульової перевірки у ваш метод розширення?
PatrickV

6
Ні. Обробник передається в метод, після чого цей екземпляр не може бути змінений.
Джуда Габріель Хіманго

@PatrickV Ні, Джуда має рацію, параметр handlerвище є параметром значення методу. Він не зміниться під час запуску методу. Для того, щоб це сталося, це повинен бути refпараметр (очевидно, що не допускається, щоб параметр мав refі thisмодифікатор, і поле), звичайно.
Jeppe Stig Nielsen

42

Для систем, які інтенсивно використовують події та є критично важливими для продуктивності , ви точно захочете принаймні подумати про те, щоб цього не робити. Вартість підняття події з порожнім делегатом приблизно вдвічі перевищує вартість підняття події за допомогою нульової перевірки.

Ось кілька цифр, що виконують тести на моїй машині:

For 50000000 iterations . . .
No null check (empty delegate attached): 530ms
With null check (no delegates attached): 249ms
With null check (with delegate attached): 452ms

І ось код, за яким я отримував ці цифри:

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
Ви жартуєте, так? Виклик додає 5 наносекунд, і ви попереджаєте це робити? Я не можу придумати більш нерозумної загальної оптимізації, ніж ця.
Бред Вілсон,

8
Цікаво. Згідно з вашими висновками, швидше перевірити наявність нуля та викликати делегата, ніж просто викликати його без перевірки. Для мене це не звучить правильно. Але в будь-якому випадку це така невелика різниця, що я не думаю, що це помітно у всіх, крім самих крайніх випадків.
Моріс

22
Бред, я спеціально сказав про критично важливі для продуктивності системи, які інтенсивно використовують події. Як це загально?
Кент Бугаарт,

3
Ви говорите про покарання за продуктивність при виклику порожніх делегатів. Я запитую вас: що відбувається, коли хтось насправді передплачує подію? Вам слід турбуватися про результати роботи передплатників, а не порожніх делегатів.
Liviu Trifoi

2
Великі витрати на продуктивність діють не в тому випадку, коли "справжніх" абонентів нульові, а в тому випадку, коли він є. У цьому випадку підписка, відписка та виклик повинні бути дуже ефективними, оскільки їм не доведеться нічого робити зі списками викликів, але той факт, що подія (з точки зору системи) матиме двох абонентів, а не одного, змусить використовувати коду, який обробляє "n" абонентів, а не код, який оптимізовано для випадку "нуль" або "один".
supercat

7

Якщо ви робите це в / lot /, вам може знадобитися один, статичний / спільний порожній делегат, який ви використовуєте повторно, просто для зменшення обсягу екземплярів делегатів. Зауважте, що компілятор у будь-якому випадку кешує цей делегат на подію (у статичному полі), тому це лише один екземпляр делегата на визначення події, тому це не величезна економія - але, можливо, варто.

Звичайно, поле для кожного екземпляра в кожному класі все одно займе однаковий простір.

тобто

internal static class Foo
{
    internal static readonly EventHandler EmptyEvent = delegate { };
}
public class Bar
{
    public event EventHandler SomeEvent = Foo.EmptyEvent;
}

Окрім цього, це здається чудовим.


1
З відповіді тут: stackoverflow.com/questions/703014/… випливає, що компілятор вже робить оптимізацію для одного екземпляра.
Говерт,

3

Немає значущого покарання за виконання, за винятком, можливо, деяких екстремальних ситуацій.

Однак зауважте, що цей трюк стає менш актуальним у C # 6.0, оскільки мова надає альтернативний синтаксис виклику делегатів, який може бути нульовим:

delegateThatCouldBeNull?.Invoke(this, value);

Вище, нульовий умовний оператор ?.поєднує перевірку нуля з умовним викликом.



2

Я б сказав, що це трохи небезпечна конструкція, тому що вона спокушає вас зробити щось на зразок:

MyEvent(this, EventArgs.Empty);

Якщо клієнт видає виняток, сервер йде з ним.

Тоді, можливо, ви робите:

try
{
  MyEvent(this, EventArgs.Empty);
}
catch
{
}

Але якщо у вас кілька абонентів, і один абонент видає виняток, що відбувається з іншими абонентами?

З цією метою я використовував деякі статичні допоміжні методи, які виконують нульову перевірку та ковтають будь-які винятки з боку абонента (це від 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
        { }
    }
}

Не для того, щоб задиратися, але не знайдіть <code> if (del == null) {return; } Делегувати [] delegate = del.GetInvocationList (); </code> є кандидатом на перегони?
Черіан

Не зовсім. Оскільки делегати є типами значень, del насправді є приватною копією ланцюга делегатів, яка доступна лише тілу методу UnsafeFire. (Застереження: це не вдається, якщо UnsafeFire вбудовується, тому вам потрібно використовувати атрибут [MethodImpl (MethodImplOptions.NoInlining)], щоб протистояти йому).
Даніель Фортунов,

1) Делегати є еталонними типами 2) Вони незмінні, тому це не умова раси 3) Я не думаю, що вбудовування змінює поведінку цього коду. Я очікував би, що параметр del стане новою локальною змінною, коли буде вбудований.
CodesInChaos

Ваше рішення для того, "що трапиться, якщо буде вилучено виняток", полягає в ігноруванні всіх винятків? Ось чому я завжди або майже завжди обгортаю всі підписки на події в спробі (використовуючи зручну Try.TryAction()функцію, а не явний try{}блок), але я не ігнорую винятки, повідомляю про них ...
Dave Cousineau

0

Замість підходу "порожній делегат" можна визначити простий метод розширення для інкапсуляції звичайного методу перевірки обробника подій на нуль. Це описано тут і тут .


-2

Відповідь на це запитання поки що пропущено: небезпечно уникати перевірки нульового значення .

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

Річ у тім, що ви ніколи не знаєте, хто яким способом використовуватиме ваш код. Ви ніколи не знаєте, чи через кілька років під час виправлення помилки вашого коду подія / обробник встановлюється як нуль.

Завжди пишіть перевірку if.

Сподіваюся, що допомагає;)

ps: Дякую за розрахунок продуктивності.

pps: відредаговано його від випадку події до прикладу зворотного виклику. Дякую за відгук ... Я "закодував" приклад без Visual Studio і пристосував приклад, який я мав на увазі, до події. Вибачте за непорозуміння.

ppps: Не знаю, чи все-таки це відповідає нитці ... але я думаю, що це важливий принцип. Будь ласка, перевірте ще один потік стеку


1
x.MyFunnyEvent = null; <- Це навіть не компілюється (поза класом). Сенс події підтримує лише + = та - =. І ви навіть не можете зробити x.MyFunnyEvent- = x.MyFunnyEvent поза класом, оскільки засіб отримання подій є квазі protected. Ви можете перервати подію лише з самого класу (або з похідного класу).
CodesInChaos

ви маєте рацію ... вірно для подій ... був випадок із простим обробником. Вибачте. Я спробую відредагувати.
Томас

Звичайно, якщо ваш делегат є загальнодоступним, це буде небезпечно, тому що ви ніколи не знаєте, що робитиме користувач. Однак, якщо ви встановите порожні делегати для приватної змінної і самі обробляєте + = і - =, це не буде проблемою, і перевірка на нуль буде безпечною для потоку.
ForceMagic
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.