Я видел несколько упоминаний этой идиомы (в том числе на SO ):
// Deliberately empty subscriber
public event EventHandler AskQuestion = delegate {};
Достоинства очевидны - это избавляет от необходимости проверять значение null перед запуском события.
Однако я очень хочу понять, есть ли у этого недостатки. Например, широко ли он используется и достаточно прозрачен, чтобы не вызывать головную боль при обслуживании? Есть ли заметное снижение производительности при вызове пустого события подписчика?
Вместо того, чтобы вызывать накладные расходы на производительность, почему бы не использовать метод расширения для решения обеих проблем:
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);
источник
handler
выше параметр является параметром значения для метода. Пока метод работает, он не изменится. Чтобы это произошло, это должен бытьref
параметр (очевидно, что параметр не может иметьref
иthis
модификатор, и модификатор) или, конечно же, поле.Для систем, которые интенсивно используют события и критичны к производительности , вы определенно захотите хотя бы подумать о том, чтобы этого не делать. Стоимость инициирования события с пустым делегатом примерно вдвое выше, чем стоимость инициирования его с помощью нулевой проверки.
Вот некоторые показатели тестов на моей машине:
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); } } } }
источник
Если вы делаете это в / lot /, вам может потребоваться один статический / общий пустой делегат, который вы будете использовать повторно, просто для уменьшения объема экземпляров делегата. Обратите внимание, что компилятор в любом случае кэширует этот делегат для каждого события (в статическом поле), поэтому для каждого определения события используется только один экземпляр делегата, так что это небольшая экономия, но, возможно, того стоит.
Конечно, поле для каждого экземпляра в каждом классе по-прежнему будет занимать одно и то же место.
т.е.
internal static class Foo { internal static readonly EventHandler EmptyEvent = delegate { }; } public class Bar { public event EventHandler SomeEvent = Foo.EmptyEvent; }
В остальном вроде нормально.
источник
Нет никаких существенных ограничений производительности, о которых можно было бы говорить, за исключением, возможно, некоторых экстремальных ситуаций.
Обратите внимание, однако, что этот трюк становится менее актуальным в C # 6.0, потому что язык предоставляет альтернативный синтаксис для вызова делегатов, которые могут быть нулевыми:
delegateThatCouldBeNull?.Invoke(this, value);
Выше условный оператор
?.
NULL сочетает проверку NULL с условным вызовом.источник
Насколько я понимаю, пустой делегат является потокобезопасным, а проверка на null - нет.
источник
Я бы сказал, что это немного опасная конструкция, потому что она соблазняет вас сделать что-то вроде:
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 { } } }
источник
Try.TryAction()
функцию, а не явныйtry{}
блок), но я не игнорирую исключения, я сообщаю о них ...Вместо подхода с «пустым делегатом» можно определить простой метод расширения, инкапсулирующий традиционный метод проверки обработчика событий на нулевое значение. Это описано здесь и здесь .
источник
Одна вещь пока упускается из виду как ответ на этот вопрос: опасно избегать проверки на нулевое значение .
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: Не знаю, подходит ли он еще к теме ... но я думаю, что это важный принцип. Пожалуйста, также проверьте другой поток стека
источник
protected
. Вы можете отделить событие только от самого класса (или от производного класса).