在C#中引发事件时忽略处理程序引发的异常

11

在C#中,引发事件时我最讨厌的事情之一是,如果事件处理程序中出现异常,会破坏我的代码,并可能阻止其他处理程序被调用,如果先调用的是已经崩溃的处理程序;在大多数情况下,我的代码并不关心正在侦听其事件的其他人的代码是否崩溃。

我创建了一个捕获异常的扩展方法:

public static void Raise(this EventHandler eh, object sender, EventArgs e)
{
  if (eh == null)
    return;
  try
  {
    eh(sender, e);
  }
  catch { }
}
虽然这意味着我的代码将继续执行,但此方法无法阻止第一个事件处理程序引发异常并防止通知事件的第二个及后续处理程序。我正在研究一种通过迭代GetInvocationList来包装每个单独的事件处理程序以进行其自己的try/catch的方法,但这似乎效率低下,而且我不确定最佳方法是什么,甚至不确定是否应该这样做。
此外,我真的不舒服简单地忽略这个异常(FxCop/Resharper也是如此); 实际上在这种情况下应该发生什么事情?

1
我经常想知道处理这个问题的最佳方法是什么。 - ChaosPandion
这实际上引出了另一个问题,我将单独提出来询问;在这个问题中,我从引发事件的类的角度进行编码,但我想知道事件处理程序是否应该首先允许抛出异常。https://dev59.com/h3A75IYBdhLWcg3wxcHV - Flynn1179
4个回答

4
如果你像这样做会怎么样呢?
public static void Raise(this EventHandler eh, object sender, EventArgs e)
{
    if (eh == null)
        return;

    foreach(var handler in eh.GetInvocationList().Cast<EventHandler>())
    {
        try
        {
            handler(sender, e);
        }
        catch { }
    }
}

实际上,我使用了 foreach (Delegate handler in WeaponChanged.GetInvocationList()) handler(sender, e);,但不幸的是,在 VS 中 handler(sender, e) 会出现错误,提示“需要方法、委托或事件”;我对此感到困惑,因为它显然是一个委托。 - Flynn1179
@Flynn1179 - Delegate本身不能以那种方式调用。你需要像我示例中所看到的那样进行转换,或者像这样调用委托:handler.DynamicInvoke(sender, e); - ChaosPandion
啊啊..我开始混淆'delegate'和'Delegate'了 :) - Flynn1179
好的,哪个更快 - 先进行强制转换然后执行 handler(sender, e),还是不进行强制转换然后执行 handler.DynamicInvoke(sender, e)?我知道我可以通过一个快速测试来解决这个问题,但我很好奇为什么其中一个更快,或者说更好。 - Flynn1179
@Flynn1179 - 正如其名称所示,调用 DynamicInvoke 可以接受任何参数,但在运行时将验证这些参数。将委托强制转换为其正确的类型将删除检查,并实际上会提高性能。实际数字很难估计,因为它取决于您有多少订阅者以及此事件被触发的频率。 - ChaosPandion
我刚刚发现了一个小问题:EventHandler<MyEventArgs> 无法转换为 EventHandler;不幸的是,泛型和非泛型的 EventHandler 类都直接继承自 MulticastDelegate,所以看来我别无选择,只能使用 DynamicInvoke 选项。 - Flynn1179

3
你应该只捕获那些你能够处理的异常。例如,你可能正在访问一个文件,但没有足够的权限。如果你知道这种情况偶尔会发生,并且只需要记录一下,那么只捕获 这个异常
如果你的代码无法处理异常,则不要捕获它。
这是一个异常情况,因此其他事件处理程序很可能也无法完成它们的工作,所以让它传播到可以安全处理的级别。

3

如果事件处理程序没有捕获异常,我不认为你有任何好的方式可以在抛出事件的类中处理异常。你没有任何上下文知道处理程序在做什么。在这种情况下,最安全的方式是让应用程序彻底崩溃,因为你不知道其他事件处理程序可能引起的副作用,这可能会破坏你的应用程序。这里的教训是事件处理程序必须是强大的(处理自己的异常),因为触发事件的类不应该捕获处理程序抛出的异常。


1
@Gabe - 一个有缺陷的插件应该会崩溃它接触到的每个应用程序,因为它是有缺陷的插件。人们应该意识到它有缺陷,以便可以修复或避免使用。以可能损害用户数据为代价来保护劣质插件是一种不良政策。 - Jeffrey L Whitledge
1
@Chaos,我以前也遇到过这种情况,即客户看到“崩溃”的成本被认为很高,因此有压力要不惜一切代价让应用程序继续运行。随着我积累更多的经验,我越来越抵制这种思路,因为让未知的故障滑过去可能会导致各种丑陋的后果。 - Dan Bryant
@supercat - 进度窗口关闭后为什么会抛出异常?那些不必要的异常为什么不能被知道它们是无用的代码(即进度条更新代码)捕获并忽略?为什么调用进度更新的长时间运行过程需要区分重要异常和不重要异常? - Jeffrey L Whitledge
@Jeffery L Whitledge:我的主要观点是,在某些情况下,有用的代码默默失败是完全合适的。例如,一个属性更新事件调用控件的BeginInvoke语义为(“如果你仍然存在,你应该显示X”); 如果控件在错误的时间被处理,则可能会失败。任何这样的事件监听器都应该捕获和抑制事件本身,而不是让它们逃脱;对于许多“属性更新事件”场景,默默失败是合理的行为。 - supercat
@Jeffery L Whitledge: ...并且这会阻止执行其他需要维护一致性的属性更新处理程序,可能会导致数据损坏,如果允许事件处理在传播异常之前运行到完成,就可以避免这种情况。当然,如果要以这种方式手动调用事件处理程序以允许在异常传播之前进行完整处理,则可以将处理程序保留为列表,而不是作为MulticastDelegate。 - supercat
显示剩余11条评论

2

一条简单但重要的规则:

尽可能早地让代码中的每个缺陷都变得致命。

这样,漏洞就能被发现和修复,软件才能不断变得更加强大。其他人所说的是正确的:永远不要捕获你没有预期和知道如何处理的异常。


1
每个人都说这很简单,但事实并非总是如此。如果他们无法控制谁订阅事件怎么办?如果必须继续处理怎么办? - ChaosPandion
1
@Chaos,在这种情况下,你需要以一种允许隔离的方式来构建你的事件。例如,使用独立进程设置客户端/服务器模型。现在,即使客户端出现故障,服务器也可以继续处理。如果你有简单的回调事件而没有隔离边界,那么就没有保护措施防止一个不守规矩的客户端干扰你的处理过程。该进程无法保证其命令(正确地继续处理)并且假装它能够这样做是不负责任的。 - Dan Bryant
丹:假设这都是托管代码,你认为有一个有缺陷的异常处理程序会如何干扰主进程? - Gabe
1
丹:是的,我指的是事件处理程序。我的意思是OP似乎遇到了代码与事件处理程序代码不耦合的情况。我的意思是,为什么要假设损坏仅限于应用程序并只崩溃应用程序呢?你不知道这个事件处理程序绑定了什么可怕的东西,所以为什么不崩溃整个操作系统呢? - Gabe
@Dan Bryant:我认为,当代码中发生意外异常并改变对象时,使相关对象无效可能是明智的(这样任何进一步尝试使用该对象都会抛出异常,可能还带有先前异常的保存副本),但我看不出为什么期望代码未预期改变的对象会出现损坏。许多事件只设计用于改变引发它们之外的对象。这类处理程序的失败不应影响引发事件的对象,也不应影响其他处理程序。 - supercat
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接