Ответ 1
Единственный недостаток - очень небольшое снижение производительности, поскольку вы вызываете лишний пустой делегат. Помимо этого не существует штрафа за обслуживание или другого недостатка.
Я видел несколько упоминаний об этой идиоме (включая на 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);
Для систем, в которых активно использовать события и критичны по производительности, вы, безусловно, захотите, по крайней мере, не подумать об этом. Стоимость поднятия события с пустым делегатом примерно в два раза выше, чем при первом запуске с нулевой проверкой.
Вот некоторые цифры, на которых запущены тесты на моей машине:
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/, вы можете захотеть иметь один, статический/общий пустой делегат, который вы повторно используете, просто для уменьшения объема экземпляров делегата. Обратите внимание, что компилятор кэширует этот делегат на событие в любом случае (в статическом поле), поэтому это только один экземпляр делегата для определения события, поэтому он не является большой экономией, но, возможно, стоит.
Поле каждого экземпляра в каждом классе по-прежнему будет занимать одно и то же пространство, конечно.
то есть.
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
{
}
Но если у вас несколько подписчиков, а один абонент выбрасывает исключение, что происходит с другими абонентами?
С этой целью я использую некоторые статические вспомогательные методы, которые выполняют нулевую проверку и проглатывают любое исключение со стороны абонента (это от имени).
// 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
{ }
}
}
Существует никаких значимых нарушений производительности, о которых можно говорить, кроме, возможно, для некоторых экстремальных ситуаций.
Обратите внимание, однако, что этот трюк становится менее актуальным в С# 6.0, потому что язык предоставляет альтернативный синтаксис вызовам делегатов, которые могут быть null:
delegateThatCouldBeNull?.Invoke(this, value);
Выше, нулевой условный оператор ?.
объединяет проверку нуля с условным вызовом.
Вместо подхода "пустой делегат" можно определить простой метод расширения, чтобы инкапсулировать обычный метод проверки обработчика событий с помощью 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
Дело в том, что вы никогда не знаете, кто будет использовать ваш код, каким образом. Вы никогда не знаете, если через несколько лет при исправлении ошибки вашего кода event/обработчик установлен в значение null.
Всегда, пишите проверку if.
Надеюсь, что помогает;)
ps: Спасибо за расчет производительности.
pps: Отредактировано это из примера события case и callback. Спасибо за отзывы... Я "закодировал" пример без Visual Studio и скорректировал пример, который я имел в виду для события. Извините за путаницу.
ppps: Не знаю, все еще подходит для темы... но я думаю, что это важный принцип. Также проверьте другой поток стека