Есть ли недостаток в добавлении анонимного пустого делегата при объявлении события?


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

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

вверх ясно-это позволяет избежать необходимости проверять значение null перед вызовом события.

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

9 79

9 ответов:

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

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

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

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

вот некоторые цифры, работающие тесты на моей машине:

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

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

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

т. е.

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

кроме этого, это кажется прекрасным.

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

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

MyEvent(this, EventArgs.Empty);

Если клиент выдает исключение, сервер идет с ним.

Так что, может быть, вы делаете:

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

но, если у вас есть несколько абонентов и один абонент выдает исключение, что происходит с другими абонентами?

С этой целью я использую некоторые статические вспомогательные методы, которые выполняют проверку null и проглатывают любое исключение со стороны абонента (это от 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
        { }
    }
}

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

delegateThatCouldBeNull?.Invoke(this, value);

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

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

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

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.

всегда, напишите если проверка.

надеюсь, что это помогает ;)

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

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

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