ChatGPT解决这个技术问题 Extra ChatGPT

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

我已经看到有人提到过这个成语(包括 on SO):

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

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

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


C
Community

为什么不使用 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);

有关通用版本,请参见此处:stackoverflow.com/questions/192980/…
顺便说一下,我的助手 trinity 库所做的正是:thehelpertrinity.codeplex.com
这不只是将空检查的线程问题转移到您的扩展方法中吗?
不。处理程序被传递到方法中,此时不能修改该实例。
@PatrickV 不,Judah 是对的,上面的参数 handler 是该方法的值参数。在方法运行时它不会改变。为此,它需要是一个 ref 参数(显然不允许一个参数同时具有 refthis 修饰符),或者当然是一个字段。
K
Kent Boogaart

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

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

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

你开玩笑的对吧?调用增加了 5 纳秒,你警告不要这样做?我想不出比这更不合理的一般优化了。
有趣的。根据您的发现,检查 null 并调用委托比仅在不检查的情况下调用它更快。对我来说听起来不对。但无论如何,这是一个很小的差异,我认为除了最极端的情况外,它在所有情况下都不明显。
布拉德,我专门针对大量使用事件的性能关键系统说。那个将军怎么样?
您正在谈论调用空代表时的性能损失。我问你:当有人真正订阅事件时会发生什么?您应该担心订户的表现,而不是空代表的表现。
输出基准测试中有一个误导性的侥幸,因为两个不同的对象用于热身和实际运行。因此,“预热”在第二次调用时无效,因为它只是预热了第二个对象。侥幸是由于第一次调用必须创建的空委托,这比简单的调用要长得多。如果您可以通过对两个调用使用相同的对象来更正结果,那就太好了。 :)
M
Maurice

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


如果事件本来只有一个订阅者(一种非常常见的情况),则虚拟处理程序将使其有两个。具有一个处理程序的事件比具有两个处理程序的事件处理效率更高。
M
Marc Gravell

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

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

IE

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

除此之外,似乎还不错。


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

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


如这里的答案所述,这两种模式都不会使事件线程安全:stackoverflow.com/questions/10282043/…
@Beachwalker 这个答案具有误导性。只要委托不为空,委托调用是安全的。 stackoverflow.com/a/6349163/2073670
S
Sergey Kalinichenko

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

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

delegateThatCouldBeNull?.Invoke(this, value);

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


“该语言提供了另一种语法......”隐藏了原始语言设计决策所带来的复杂性和潜在的多线程问题(基本上需要始终将委托包装在空值检查中。)我建议这种语法是一种反模式并被添加支持已成为常态的不良编码习惯。
@tekHedd 再深入一点,我们应该质疑空引用本身的引入。发明了这个不幸的东西的查尔斯·安东尼·理查德·霍尔爵士(Sir Charles Antony Richard Hoare)称这是他的十亿美元错误。就个人而言,我认为他的估计过于谦虚:今天,这个错误的成本可能接近万亿美元大关。这就是为什么我认为将 C# 转向可选可空性的决定是朝着正确方向迈出的一步。
S
Scott P

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

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

不是吹毛求疵,但你不要考虑 if (del == null) { return; } Delegate[] delegates = del.GetInvocationList(); 是竞争条件候选者吗?
不完全的。由于委托是值类型,del 实际上是委托链的私有副本,只能由 UnsafeFire 方法主体访问。 (警告:如果 UnsafeFire 被内联,这将失败,因此您需要使用 [MethodImpl(MethodImplOptions.NoInlining)] 属性来反对它。)
1)委托是引用类型 2)它们是不可变的,所以它不是竞争条件 3)我不认为内联确实改变了这段代码的行为。我希望参数 del 在内联时成为一个新的局部变量。
您对“如果抛出异常会发生什么”的解决方案是忽略所有异常?这就是为什么我总是或几乎总是将所有事件订阅包装在 try 中(使用方便的 Try.TryAction() 函数,而不是明确的 try{} 块),但我不会忽略异常,我会报告它们......
C
Community

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


C
Community

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

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


x.MyFunnyEvent = null; <- 这甚至不能编译(在类之外)。事件的重点是只支持+=和-=。而且你甚至不能在课堂之外做 x.MyFunnyEvent-=x.MyFunnyEvent 因为事件获取器是准 protected。您只能从类本身(或从派生类)中断事件。
你是对的......对于事件来说是正确的......有一个带有简单处理程序的案例。对不起。我会尝试编辑。
当然,如果您的代表是公开的,那将是危险的,因为您永远不知道用户会做什么。但是,如果您在私有变量上设置空委托并自己处理 += 和 -= ,那将不是问题,并且 null 检查将是线程安全的。

关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅