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


82

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

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

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

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

Ответы:


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/…
Бенджол,


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
Интересно. Согласно вашим выводам, быстрее проверить наличие null и вызвать делегата, чем просто вызвать его без проверки. Мне это не кажется правильным. Но в любом случае это такая маленькая разница, что я не думаю, что она заметна во всех, кроме самых крайних случаев.
Морис

22
Брэд, я специально сказал о критичных к производительности системах, которые интенсивно используют события. Как это вообще?
Kent Boogaart 05

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/… видно, что компилятор уже выполняет оптимизацию для одного экземпляра.
Govert

3

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

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

delegateThatCouldBeNull?.Invoke(this, value);

Выше условный оператор ?.NULL сочетает проверку NULL с условным вызовом.



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 [] delegates = del.GetInvocationList (); </code> является кандидатом в состояние гонки?
Cherian

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

1) Делегаты являются ссылочными типами 2) Они неизменяемы, поэтому это не состояние гонки 3) Я не думаю, что встраивание действительно меняет поведение этого кода. Я ожидал, что параметр del станет новой локальной переменной при встраивании.
CodesInChaos,

Ваше решение «что произойдет, если возникнет исключение» - игнорировать все исключения? Вот почему я всегда или почти всегда заключаю все подписки на события в попытку (используя удобную Try.TryAction()функцию, а не явный try{}блок), но я не игнорирую исключения, я сообщаю о них ...
Дэйв Кузино,

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

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

Всегда пишите чек, если.

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

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

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

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


1
x.MyFunnyEvent = ноль; <- Это даже не компилируется (вне класса). Смысл события только в поддержке + = и - =. И вы даже не можете выполнить x.MyFunnyEvent- = x.MyFunnyEvent вне класса, поскольку средство получения события является квази protected. Вы можете отделить событие только от самого класса (или от производного класса).
CodesInChaos,

ты прав ... правда по событиям ... был случай с простым обработчиком. Сожалею. Попробую отредактировать.
Thomas

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