82

我已经看到一些关于这个成语的提及(包括在 SO 上):

// Deliberately empty subscriber
public event EventHandler AskQuestion = delegate {};

好处很明显 - 它避免了在引发事件之前检查 null 的需要。

但是,我很想知道是否有任何缺点。 例如,它是否被广泛使用并且足够透明以至于不会引起维护头痛?空事件订阅者调用是否有明显的性能影响?

4

9 回答 9

46

与其引入性能开销,不如使用扩展方法来缓解这两个问题:

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);
于 2008-11-19T18:27:45.263 回答
42

对于大量使用事件并且对性能至关重要的系统,您肯定希望至少考虑不这样做。使用空委托引发事件的成本大约是首先使用空检查引发事件的成本的两倍。

以下是在我的机器上运行基准测试的一些数据:

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);
            }
        }
    }
}
于 2008-10-04T23:56:31.890 回答
36

唯一的缺点是非常轻微的性能损失,因为您调用了额外的空委托。除此之外,没有维护费用或其他缺点。

于 2008-10-04T19:53:48.183 回答
7

如果您正在执行 /lot/,您可能希望拥有一个可重复使用的单个静态/共享空委托,只是为了减少委托实例的数量。请注意,编译器无论如何都会缓存每个事件的这个委托(在静态字段中),因此每个事件定义只有一个委托实例,所以它不是一个巨大的节省 - 但也许值得。

当然,每个类中的每个实例字段仍将占用相同的空间。

IE

internal static class Foo
{
    internal static readonly EventHandler EmptyEvent = delegate { };
}
public class Bar
{
    public event EventHandler SomeEvent = Foo.EmptyEvent;
}

除此之外,似乎还不错。

于 2008-10-04T20:54:51.353 回答
3

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

于 2008-11-05T04:55:44.917 回答
3

除了可能的一些极端情况外,没有什么有意义的性能损失可谈。

但是请注意,这个技巧在 C# 6.0 中变得不那么相关了,因为该语言提供了一种替代语法来调用可能为 null 的委托:

delegateThatCouldBeNull?.Invoke(this, value);

上面,空条件运算符?.结合了空值检查和条件调用。

于 2015-08-10T18:23:23.570 回答
2

我会说这有点危险,因为它会诱使您执行以下操作:

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
        { }
    }
}
于 2008-11-05T05:41:17.973 回答
0

可以定义一个简单的扩展方法来封装检查事件处理程序是否为空的常规方法,而不是“空委托”方法。它在此处此处进行了描述。

于 2009-03-30T19:17:52.203 回答
-2

到目前为止,这个问题的答案漏掉了一件事:避免检查空值是很危险的

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:不知道它是否仍然适合线程......但我认为这是一个重要的原则。另请检查stackflow的另一个线程

于 2010-10-23T08:56:05.150 回答