Есть ли обратная сторона в добавлении анонимного пустого делегата при объявлении события? - PullRequest
81 голосов
/ 04 октября 2008

Я видел несколько упоминаний об этой идиоме (включая на SO ):

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

Преимущество очевидно - оно устраняет необходимость проверки на ноль, прежде чем поднять событие.

Однако я хочу понять, есть ли какие-либо недостатки. Например, это то, что широко используется и достаточно прозрачно, чтобы не вызывать головной боли при обслуживании? Есть ли заметное снижение производительности при пустом вызове подписчика на событие?

Ответы [ 9 ]

46 голосов
/ 19 ноября 2008

Вместо того, чтобы вызывать снижение производительности, почему бы не использовать метод расширения для решения обеих проблем:

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);
42 голосов
/ 05 октября 2008

Для систем, которые интенсивно используют события и критически важны для производительности , вы определенно захотите по крайней мере рассмотреть , не делающие этого. Стоимость поднятия события с пустым делегатом примерно в два раза выше, чем для поднятия его с пустой проверкой.

Вот некоторые цифры, на которых запущены тесты на моей машине:

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);
            }
        }
    }
}
36 голосов
/ 04 октября 2008

Единственным недостатком является очень небольшое снижение производительности, поскольку вы вызываете дополнительный пустой делегат. Кроме этого, нет никакого штрафа за обслуживание или другого недостатка.

7 голосов
/ 05 октября 2008

Если вы делаете это в / lot /, вы можете захотеть иметь один статический / общий пустой делегат, который вы используете повторно, просто чтобы уменьшить объем экземпляров делегатов. Обратите внимание, что компилятор кэширует этот делегат для каждого события в любом случае (в статическом поле), так что это только один экземпляр делегата на определение события, поэтому это не огромное сохранение - но, возможно, стоит.

Поле каждого экземпляра в каждом классе, конечно же, будет занимать одно и то же место.

т.е.

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

Кроме этого, все в порядке.

2 голосов
/ 10 августа 2015

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

Обратите внимание, однако, что этот трюк становится менее актуальным в C # 6.0, потому что язык предоставляет альтернативный синтаксис для вызова делегатов, который может быть нулевым:

delegateThatCouldBeNull?.Invoke(this, value);

Выше условный оператор null ?. объединяет проверку нуля с условным вызовом.

2 голосов
/ 05 ноября 2008

Я бы сказал, что это немного опасная конструкция, потому что она соблазняет вас сделать что-то вроде:

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
        { }
    }
}
2 голосов
/ 05 ноября 2008

Насколько я понимаю, пустой делегат является потокобезопасным, а проверка на нуль - нет.

0 голосов
/ 23 октября 2010

Пока что в качестве ответа на этот вопрос упущено одно: Опасно избегать проверки на нулевое значение .

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

Дело в том, что вы никогда не знаете, кто каким образом будет использовать ваш код. Вы никогда не узнаете, если через несколько лет во время исправления ошибки в вашем коде событие / обработчик будет иметь значение null.

Всегда пишите проверку if.

Надеюсь, что поможет;)

PS: Спасибо за расчет производительности.

pps: отредактировал его из случая события в пример обратного вызова. Спасибо за отзыв ... Я "закодировал" пример без Visual Studio и приспособил пример, который имел в виду, к событию. Извините за путаницу.

ppps: не знаю, подходит ли он по-прежнему к теме ... но я думаю, что это важный принцип. Также проверьте другой поток в стеке

0 голосов
/ 30 марта 2009

Вместо подхода «пустой делегат» можно определить простой метод расширения, который инкапсулирует обычный метод проверки обработчика событий на нуль. Описано здесь и здесь .

...