在事件声明中添加匿名空委托是否有不利之处?


82

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

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

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

但是,我很想了解是否有任何弊端。 例如,它是否已经被广泛使用并且足够透明以至于不会引起维护麻烦?空事件订阅者调用是否有明显的性能下降?

Answers:


36

唯一的缺点是调用额外的空代理会导致轻微的性能损失。除此之外,没有维护罚款或其他缺点。


19
如果该事件原本只有一个订户(一种非常常见的情况),则虚拟处理程序将使它拥有两个订户。一个处理事件的处理很多比那些有两个更有效。
2012年

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

2
通用版本请参见此处:stackoverflow.com/questions/192980/…– Benjol
2009年


3
这不只是将null检查的线程问题转移到您的扩展方法中吗?
PatrickV

6
否。将Handler传递到方法中,此时无法修改该实例。
Judah Gabriel Himango 2013年

@PatrickV不,犹大是对的,handler上面的参数是该方法的值参数。该方法运行时不会更改。为此,它必须是一个ref参数(显然不允许参数同时具有refthis修饰符)或一个字段。
Jeppe Stig Nielsen 2014年

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

11
你在开玩笑,对吧?调用会增加5纳秒,并且您警告不要这样做?我认为没有比这更合理的常规优化了。
布拉德·威尔逊,

8
有趣。根据您的发现,检查空值并调用委托要比不进行检查的调用更快。对我来说听起来不对。但是无论如何,这是一个很小的差异,我认为除了最极端的情况以外,其他所有情况都不明显。
莫里斯

22
布拉德,我特别提到了对于大量使用事件的性能至关重要的系统。一般如何?
肯特·布加亚特

3
您在谈论调用空代理时的性能损失。我问你:当有人真正订阅该活动时会发生什么?您应该担心订户的性能,而不是空代理的性能。
Liviu Trifoi 2011年

2
在“真实”订户为零的情况下,巨大的性能成本并没有出现,而在“订户”为零的情况下却没有。在那种情况下,订阅,取消订阅和调用应该非常有效,因为它们不必对调用列表做任何事情,但是事件(从系统的角度来看)将有两个订阅者而不是一个订阅者,这将强制使用处理“ n”个订户的代码的一部分,而不是针对“零”或“一个”情况优化的代码。
2012年

7

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

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

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

除此之外,看起来还不错。


1
从这里的答案看来:stackoverflow.com/questions/703014/…编译器已经对单个实例进行了优化。
Govert

3

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

但是请注意,此技巧在C#6.0中变得不那么重要了,因为该语言为调用可能为null的委托提供了另一种语法:

delegateThatCouldBeNull?.Invoke(this, value);

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



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

不是无所事事,但如果(del == null){return; } Delegate []代表= del.GetInvocationList(); </ code>是竞争条件的候选者吗?
谢里安

不完全的。由于委托是值类型,因此del实际上是委托链的私有副本,仅UnsafeFire方法主体可以访问。(注意:如果内联UnsafeFire,此操作将失败,因此您需要使用[MethodImpl(MethodImplOptions.NoInlining)]属性对其进行验证。)
Daniel Fortunov,2009年

1)代表是引用类型2)它们是不可变的,因此它不是竞争条件3)我不认为内联不会改变此代码的行为。我希望内联时参数del成为新的局部变量。
CodesInChaos 2010年

您对“如果引发异常会发生什么情况”的解决方案是忽略所有异常?这就是为什么我总是或几乎总是尝试包装所有事件订阅(使用方便的Try.TryAction()功能,而不是显式的try{}块)的原因,但是我不忽略异常,我向他们报告了这些情况
Dave Cousineau

0

代替“空委托”方法,可以定义一种简单的扩展方法,以封装检查事件处理程序是否为null的常规方法。这里这里都有描述。


-2

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


1
x.MyFunnyEvent = null; <-甚至不编译(在类外部)。事件的重点仅支持+ =和-=。而且您甚至不能在类之外执行x.MyFunnyEvent- = x.MyFunnyEvent,因为事件获取器是准的protected。您只能从类本身(或派生类)中中断事件。
CodesInChaos

您是正确的...对于事件来说是正确的...有一个简单的处理程序的案例。抱歉。我将尝试编辑。
托马斯

当然,如果您的代表是公开的,那将很危险,因为您永远不知道用户会做什么。但是,如果在私有变量上设置了空的委托,并且自己处理+ =和-=,那将不是问题,并且null检查将是线程安全的。
ForceMagic 2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.