【问题标题】:Exception handling (contradicting documentation / try-finally vs. using)异常处理(与文档相矛盾/try-finally 与 using)
【发布时间】:2017-07-04 04:26:16
【问题描述】:

我以为我已经了解 C# 中的异常处理是如何工作的。重新阅读文档以获得乐趣和自信,我遇到了问题:

This document 声称以下两个代码 sn-ps 是等价的,甚至更多的是,第一个在编译时被翻译成后一个。

using (Font font1 = new Font("Arial", 10.0f)) {
  byte charset = font1.GdiCharSet;
}

和

{
  Font font1 = new Font("Arial", 10.0f);
  try {
    byte charset = font1.GdiCharSet;
  }
  finally {
    if (font1 != null)
      ((IDisposable)font1).Dispose();
  }
}

此外,它声称:

using 语句确保 Dispose 被调用,即使 在对象上调用方法时发生异常。

相比之下,that document 声明:

在处理的异常中,保证关联的finally 块 要运行。但是,如果异常未处理,则执行 finally 块取决于异常展开操作的方式 触发。

我没有把它放在一起。在第一个文档的代码示例中,异常显然是未处理的(因为没有 catch 块)。现在,如果第二个文档中的语句为真,则finally 块不保证执行。这最终与第一个文件所说的相矛盾(“using 声明确保 ...”)(强调我的)。

那么真相是什么?

编辑 1

我还是不明白。 StevieB 的回答让我阅读了 C# 语言规范中的更多部分。在第 16.3 节中,我们有:

[...] 这个搜索一直持续到找到一个 catch 子句可以 处理当前异常 [...] 一旦匹配的 catch 子句 发现,系统准备将控制权转移到第一条语句 的 catch 子句。在 catch 子句开始执行之前, 系统首先按顺序执行任何 finally 子句 与 try 语句相关联的嵌套比 捕获了异常。

所以我制作了一个简单的测试程序,其中包含产生除以零的代码,并且位于try 块内。我的任何代码中都不会捕获该异常,但相应的 try 语句有一个 finally 块:

int b = 0;

try {
  int a = 10 / b;
}
finally {
  MessageBox.Show("Hello");
}

最初,根据上面的文档 sn-p,我曾预计 finally 块永远不会被执行,并且程序在没有附加调试器的情况下执行时会死掉。但这种情况并非如此;而是显示了我们都非常熟悉的“异常对话框”,然后出现了“Hello”对话框。

在考虑了一段时间并阅读了文档、文章和诸如this 和that 之类的问题之后,很明显,这个“异常对话框”是由内置于 Application 的标准异常处理程序产生的。 Run() 和其他可以“启动”你的程序的常用方法,所以我不再想知道 为什么finally 块会运行。

但我仍然完全感到困惑,因为“Hello”对话框出现在“异常对话框”之后。上面的文档 sn-p 很清楚(好吧,可能我又太傻了):

CLR 找不到与发生被零除的try 语句关联的catch 子句。所以它应该将异常向上一级传递给调用者,在那里也找不到匹配的catch 子句(那里甚至没有try 语句)等等(如上所述,我不处理(即捕获)此测试程序中的任何异常)。

最后,异常应该满足 CLR 的默认捕获所有异常处理程序(即默认情况下在 Application.Run() 及其朋友中处于活动状态的异常处理程序),但是(根据上面的文档)CLR 现在应该执行所有finally 块嵌套比默认处理程序更深(“我的”finally 块属于这些,不是吗?)在执行 CLR 捕获所有默认处理程序的 @987654345 @块。

这意味着“Hello”对话框应该出现在“异常对话框”之前,不是吗?嗯,很明显,情况正好相反。有人可以详细说明一下吗?

【问题讨论】:

  • 有点不清楚你在问什么。您基本上是在问using 是否会 100% 处理?因为如果是这样,你是对的,不,它不会(必然)因为它受到在它“包含”的代码期间引发的那种异常的影响,类似于你可能更熟悉的try catch .
  • 我相信“未处理”是指应用程序未处理异常,即异常将导致应用程序结束/崩溃。由于应用程序正在终止,因此不一定需要执行 finally 块。
  • @JᴀʏMᴇᴇ:我猜他是在问finally 是否有可能不会在未处理的异常上执行,或者 异常展开操作 是什么意思。 MSDN 中的链接 Unhandled Exception Processing in the CLR 已失效
  • "... 异常显然未处理..." - 仅在 那个 代码块中。在调用堆栈的上层可以有 catch 块。
  • @JᴀʏMᴇᴇ 我在问哪个文件是正确的。显然,其中一个肯定是错的,除非我已经失去了我内心的其余逻辑思维......

标签: c# exception try-catch using


【解决方案1】:

本文档声称以下两个代码sn-ps是等价的

他们是。

using 语句可确保调用 Dispose,即使在您调用对象上的方法时发生异常。

差不多。

这最终与第一个文件所说的相矛盾

嗯,第一个有点太模糊了,而不是完全不正确。

在某些情况下finally 不会运行,包括using 所暗示的情况。 StackOverflowException 就是一个例子(一个真正的溢出堆栈,如果你只做throw new StackOverflowException(),finally 就会运行)。

所有示例都是您无法捕捉到的东西,而且您的应用程序正在崩溃,因此如果从using 清理仅在应用程序运行时很重要,那么finally 就可以了。

如果即使程序崩溃,清理也很重要,那么finally 永远不够,因为它无法处理例如拔掉电源插头,在这种情况下,即使在碰撞中清理也很重要,这是需要考虑的情况。

如果异常被进一步捕获并且程序继续运行,finally 将运行。

对于未被捕获的可捕获异常,finally 块通常会运行,但仍有一些异常。一种是try-finally 在终结器中,而try 花了很长时间;在终结者队列上一段时间后,应用程序将快速失败。

【讨论】:

    【解决方案2】:

    如果您对“未处理的异常”的定义(它必须由 same try 块中的 catch 子句处理)是正确的,那么就没有理由允许构造

    try {
       ...
    }
    finally {
       ...
    }
    

    根据您的定义,finally 块永远不会运行。由于上述构造是有效的,我们必须得出结论,您对“未处理异常”的定义是不正确的。

    意思是“如果异常没有被any异常处理器处理,在调用堆栈的任何地方”。

    【讨论】:

    • 你说得对,在这种情况下,我的理解是:未处理的异常是未被 catch 块捕获的异常属于各自的 try 块我>。显然,我误解了“未处理”这个术语(我很清楚我可以在堆栈上的所有调用者中捕获异常,但并不认为这在那种情况下很重要)。
    【解决方案3】:

    您必须给文档留一点余地。在大多数情况下,都隐含着“在合理范围内”。例如,如果我关闭计算机,则不会调用任何 finally 块或 using/Disposes。

    除了关闭计算机之外,操作系统还可以在少数情况下终止您的应用程序——有效地关闭它。在这些情况下,Dispose 和 finally 不会被调用。这些通常是非常严重的错误情况,例如 out of stack 或 out of memory。在一些复杂的场景中,可能会发生非异常异常。例如,如果您有在后台线程上创建托管对象的本机代码,并且该托管对象和该线程上的某些内容引发本机异常,则可能不会调用托管异常处理程序(例如Dispose),因为操作系统只会终止线程,并且任何可能已经被释放的东西都无法再访问了。

    但是,是的,这些语句实际上是等效的,并且在合理范围内将执行 finally 块并调用 Dispose。

    【讨论】:

      【解决方案4】:

      C# 语言规范声明 finally 块将针对 System.Exception 或其任何派生异常执行(在 using 语句或其他地方)。当然,如果您遇到无法使用通常的 try..catch 逻辑处理的异常,例如AccessViolationException 所有的赌注都取消了,这就是歧义的来源。

      该规范随 Visual Studio 2013 及更高版本安装 - 2017 位于 C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\VC#\Specifications\1033

      在部分

      8.9.5 抛出语句

      我们看到以下内容:

      当抛出异常时,控制权被转移 到封闭的 try 语句中的第一个 catch 子句,它可以 处理异常。从点发生的过程 异常被抛出到将控制权转移到 合适的异常处理程序称为异常传播。 异常的传播包括重复评估 执行以下步骤,直到匹配异常的 catch 子句 成立。在此描述中,投掷点最初是位置 抛出异常的位置。

      • 在当前函数成员中, 检查包含抛出点的每个 try 语句。对于每个 语句 S,从最里面的 try 语句开始,以 最外层的 try 语句,评估以下步骤:
        • 如果 S 的 try 块包含了投掷点,如果 S 有一个或多个 catch 子句,按出现顺序检查 catch 子句 为异常找到合适的处理程序。第一个 catch 子句 指定异常类型或异常类型的基类型 被视为匹配。一般的 catch 子句(§8.10)被认为是 匹配任何异常类型。如果找到匹配的 catch 子句, 异常传播是通过将控制权转移到 该 catch 子句的块。
        • 否则,如果 try 块或 catch S 的块包围了投掷点,如果 S 有一个 finally 块, 控制权转移到 finally 块。如果 finally 块 抛出另一个异常,当前异常的处理是 终止。否则,当控制到达终点时 finally 块,继续处理当前异常。
      • 如果 异常处理程序不在当前函数中 调用,函数调用被终止,并且其中之一 发生以下情况:
        • 如果当前函数是非异步的,步骤 上面对带有抛出点的函数的调用者重复 对应于函数成员所在的语句 调用。
        • 如果当前函数是异步且返回任务的,则 异常记录在return task中,放入faulted 或 §10.14.1 中所述的取消状态。
        • 如果当前函数 是异步且返回无效的, §10.14.2 中所述通知当前线程。
      • 如果 异常处理终止所有函数成员调用 当前线程,表明该线程没有处理程序 异常,则线程本身终止。此类的影响 终止是实现定义的。

      【讨论】:

      • 这是一堵大墙引用的文字 - 也许如果您强调您认为在这里最相关的关键文字?
      【解决方案5】:

      我相信(如果我错了,请纠正我)答案就在定义中:

      using 语句确保 Dispose 被调用,即使 在对象上调用方法时发生异常。

      所以如果使用对象调用的任何方法出现异常,finally 都可以保证运行。另一方面,如果在 using 块内调用与对象无关的其他方法导致异常,则 finally 不能保证运行

      【讨论】:

      • 感谢您提请我们注意该定义,但我认为答案并不存在。根据其他答案和我之前读过的内容,这取决于堆栈上的任何调用者是否处理(捕获)了异常。
      • C# 规范 不包含任何这样的措辞。我怀疑这是 C# Reference 的作者试图为某一点添加更多解释并造成混乱的另一个例子。
      【解决方案6】:

      首先,我想提醒您,using 语句不能用于所有类型。这只能用于实现 IDisposable 接口的类型,该接口具有自动处置对象的功能。这在您提到的第二个文档中存在

      C# 还包含 using 语句,它以方便的语法为 IDisposable 对象提供类似的功能。

      这意味着如果发生未处理的异常,则对象的清理由该类型的 Dispose() 方法处理(这是为使用语句文档提供的)

      进入查询,即使您的 finally 块(生成)不能保证运行未处理的异常,对象处置操作在运行时由 .Net CLR 处理

      希望这能消除你的疑虑

      【讨论】:

      • 我看不出这是如何回答问题的
      • 有趣的方面,但它没有回答我的问题。
      • 我想知道 OP 问题的哪一部分错误使用了using
      猜你喜欢
      • 2018-07-22
      • 2012-08-23
      • 1970-01-01
      • 1970-01-01
      • 2020-11-18
      • 1970-01-01
      • 2014-10-12
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多