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

时间:2008-10-04 19:41:15

标签: c# coding-style delegates events idioms

我已经看到了一些关于这个习语的提及(包括on SO):

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

好处很明显 - 它避免了在提升事件之前检查空值的必要性。

但是,我很想知道是否存在任何缺点。例如,它是否广泛使用并且足够透明以至于不会导致维护问题?空事件用户呼叫是否有明显的性能影响?

9 个答案:

答案 0 :(得分:46)

为什么不use an extension method来缓解这两个问题,而不是引起性能开销:

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);

答案 1 :(得分: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);
            }
        }
    }
}

答案 2 :(得分:36)

唯一的缺点是,当您调用额外的空委托时,会有轻微的性能损失。除此之外,没有维护惩罚或其他缺点。

答案 3 :(得分:7)

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

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

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

除此之外,似乎还不错。

答案 4 :(得分:2)

据我所知,空委托是线程安全的,而空检查则不是。

答案 5 :(得分:2)

我会说这是一个危险的构造,因为它诱使你做类似的事情:

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
        { }
    }
}

答案 6 :(得分:2)

除了可能的某些极端情况之外,没有任何有意义的性能损失。

但请注意,此技巧在C#6.0中的相关性较低,因为该语言为调用可能为null的委托提供了另一种语法:

delegateThatCouldBeNull?.Invoke(this, value);

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

答案 7 :(得分:0)

可以定义一个简单的扩展方法来封装传统的检查事件处理程序的方法,而不是“空委托”方法。它描述为herehere

答案 8 :(得分:-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 check。

希望有所帮助;)

ps:感谢您的性能计算。

pps:从事件案例和回调示例编辑它。感谢您的反馈......我“编写”了没有Visual Studio的示例,并将我想到的示例调整为一个事件。对不起,感到困惑。

ppps:不知道它是否仍适合线程...但我认为这是一个重要的原则。另请查看another thread of stackflow