我已经看到了一些关于这个成语的提法(包括SO):
// Deliberately empty subscriber
public event EventHandler AskQuestion = delegate {};
好处很明显-避免了在引发事件之前检查null的需要。
但是,我很想了解是否有任何弊端。 例如,它是否已经被广泛使用并且足够透明以至于不会引起维护麻烦?空事件订阅者调用是否有明显的性能下降?
Answers:
为什么不使用扩展方法来缓解这两个问题,而不要引起性能开销:
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中变得不那么重要了,因为该语言为调用可能为null的委托提供了另一种语法:
delegateThatCouldBeNull?.Invoke(this, value);
上面,空条件运算符?.
将空检查与条件调用结合在一起。
我会说这有点危险,因为它会诱使您执行以下操作:
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{}
块)的原因,但是我不忽略异常,我向他们报告了这些情况
到目前为止,遗漏了一件事作为该问题的答案:避免检查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。
始终写if检查。
希望能有所帮助;)
ps:谢谢您的性能计算。
pps:从事件案例到回调示例对其进行了编辑。感谢您的反馈...我“编码”了没有Visual Studio的示例,并根据事件调整了我想到的示例。对困惑感到抱歉。
ppps:不知道它是否仍然适合线程...但是我认为这是一个重要原则。请同时检查另一个堆栈流线程
protected
。您只能从类本身(或派生类)中中断事件。