在事件声明中添加一个匿名的空委托是否有缺点?
我曾经见过一些这样的成语(包括SO ):
// Deliberately empty subscriber public event EventHandler AskQuestion = delegate {};
好处很明显 – 它避免了在提高事件之前检查空值的需要。
不过,我很想知道是否有任何缺点。 例如,它是否被广泛使用,是否透明,不会造成维修头痛? 是否有空的事件订户呼叫有明显的性能影响?
唯一的缺点是你要调用额外的空代表,这是一个非常轻微的性能损失。 除此之外,不存在维修处罚或其他缺点。
为了不引起性能开销,为什么不使用扩展方法来缓解这两个问题:
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);
对于大量使用事件且对性能至关重要的系统 ,您一定会考虑不这样做。 用一个空的委托筹集一个事件的成本大约是用一个空检查提高它的成本的两倍。
以下是我的机器上运行基准testing的一些数字:
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 /,你可能想要一个单一的静态/共享的空委托,你可以重用,只是为了减less委托实例的数量。 注意,编译器无论如何都会caching这个委托(在静态字段中),所以每个事件定义只有一个委托实例,所以这不是一个巨大的节约 – 但也许值得。
当然,每个类中的每个实例字段仍将占用相同的空间。
即
internal static class Foo { internal static readonly EventHandler EmptyEvent = delegate { }; } public class Bar { public event EventHandler SomeEvent = Foo.EmptyEvent; }
除此之外,似乎很好。
这是我的理解是空的委托是线程安全的,而空检查不是。
我会说这是一个危险的构造,因为它诱使你做这样的事情:
MyEvent(this, EventArgs.Empty);
如果客户端抛出一个exception,服务器就会使用它。
那么,也许你会这样做:
try { MyEvent(this, EventArgs.Empty); } catch { }
但是,如果您有多个订阅者,并且一个订阅者抛出exception,其他订阅者会发生什么情况?
为此,我一直使用一些静态帮助器方法进行空检查,并从订户端(这是从idesign)吞下任何exception。
// 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 { } } }
除了可能的情况,对于一些极端的情况,没有什么意义的性能损失可言。
但是请注意,这个技巧在C#6.0中变得不那么重要,因为这种语言提供了一种替代语法来调用可能为null的委托:
delegateThatCouldBeNull?.Invoke(this, value);
上面是空条件运算符?.
将空检查与条件调用相结合。
可以定义一个简单的扩展方法来封装检查事件处理程序的传统方法,而不是“空的委托”方法。 这里和这里都有描述。
到目前为止,这个问题的答案错过了一件事: 避免检查空值是危险的 。
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
事情是:你永远不知道谁会使用你的代码。 你永远不知道,如果在你的代码错误修复的某些年,事件/处理程序设置为空。
总是写下if检查。
希望有所帮助;)
ps:感谢性能计算。
pps:从事件案例编辑到callback示例。 感谢您的反馈…我编写了一个没有Visual Studio的例子,并且调整了我想到的一个事件的例子。 对困惑感到抱歉。
ppps:不知道它是否还适合线程…但我认为这是一个重要的原则。 请检查另一个stackflow的线程