在事件声明中添加匿名的空委托是否有缺点

本文关键字:是否 有缺点 声明 事件 添加 | 更新日期: 2023-09-27 17:47:25

我看到过一些关于这个习语的提及(包括在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);
            }
        }
    }
}

唯一的缺点是调用额外的空委托时会带来非常轻微的性能损失。除此之外,没有维修罚款或其他缺点。

如果您使用/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);

上面,null条件运算符?.将null检查与条件调用相结合。

我的理解是,空委托是线程安全的,而空检查则不是。

我认为这是一个有点危险的构造,因为它会诱使你做一些类似于的事情

MyEvent(this, EventArgs.Empty);

如果客户端抛出异常,服务器也会随之执行

那么,也许你会这样做:

try
{
  MyEvent(this, EventArgs.Empty);
}
catch
{
}

但是,如果您有多个订阅者,其中一个订阅者抛出异常,那么其他订阅者会发生什么?

为此,我一直在使用一些静态助手方法,这些方法可以进行null检查并从订阅者端接收任何异常(这来自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
        { }
    }
}

可以定义一个简单的扩展方法来封装检查事件处理程序是否为null的传统方法,而不是"空委托"方法。这里和这里都有描述。

到目前为止,这个问题的答案遗漏了一件事:避免检查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:不知道它是否仍然适合线程。。。但我认为这是一个重要的原则。还请检查堆叠流的另一个线程