【问题标题】:Changing an unhandled exception to a handled one in a finally block在 finally 块中将未处理的异常更改为已处理的异常
【发布时间】:2015-01-17 00:54:20
【问题描述】:

考虑这个程序:

using System;
static class Program {
  static void Main(string[] args) {
    try {
      try { throw new A(); }
      finally { throw new B(); }
    }
    catch (B) { }
    Console.WriteLine("All done!");
  }
}

class A : Exception { }

class B : Exception { }

这里抛出了一个A 类型的异常,它没有处理程序。在finally 块中,抛出B 类型的异常,该异常有一个处理程序。通常,finally 块中抛出的异常获胜,但未处理的异常则不同。

调试时,当A被抛出时调试器停止执行,并且不允许finally块被执行。

当不调试时(从命令提示符单独运行),会显示一条消息(打印和崩溃对话框)关于未处理的异常,但在那之后,“全部完成!”确实被打印出来了。

当添加一个仅重新抛出捕获的异常的顶级异常处理程序时,一切都很好:没有意外消息,并且“全部完成!”被打印出来了。

我了解这是如何发生的:在任何finally 块被执行之前确定异常是否有处理程序。这通常是可取的,并且当前的行为是有意义的。 finally 块通常不应该抛出异常。

但是this other Stack Overflow question 引用了 C# 语言规范并声称需要 finally 块来覆盖 A 异常。阅读规范,我同意这正是它所需要的:

  • 在当前函数成员中,检查每个包含抛出点的try 语句。对于每个语句S,从最里面的 try 语句开始到最外面的 try 语句结束,评估以下步骤:
    • 如果S 的try 块包含抛出点并且如果S 有一个或多个catch 子句,则检查catch 子句[...]
    • 否则,如果try 块或S 的catch 块包含抛出点,并且如果S 具有finally 块,则控制将转移到finally 块。如果finally 块抛出另一个异常,则终止当前异常的处理。否则,当控制到达finally 块的终点时,继续处理当前异常。
  • 如果在当前函数调用中未找到异常处理程序,则函数调用将终止,并出现以下情况之一:
    • [...]
  • 如果异常处理终止了当前线程中的所有函数成员调用,表明该线程没有异常的处理程序,则该线程本身终止。这种终止的影响是由实现定义的。

根据我对规范的阅读,直到所有函数调用都已终止,并且函数调用在 finally 处理程序执行之前不会终止,否则不会将异常视为未处理.

是我遗漏了什么,还是微软的 C# 实现与他们自己的规范不一致?

【问题讨论】:

  • 评论不用于扩展讨论;这个对话是moved to chat。
  • 措辞有点笨拙,可能是故意的。但是 CLR 规范对此毫无疑问,第 I.12.4.2.5 章清楚地表明 finally 块仅在 搜索并找到处理程序之后才会执行。

标签: c# .net exception-handling language-lawyer


【解决方案1】:

我认为问题在于 .NET 异常处理是如何建立在结构化异常处理之上的,结构化异常处理对于在 finally 块内抛出的规则略有不同。

当异常 A 发生时,SEH 尝试找到第一个能够处理您的异常类型的处理程序,然后开始运行所有 finally 块,展开到它,但是基于 SEH 逻辑,没有这样的处理程序,因此它会在之前为未处理的异常哭泣.NET 可以强制执行自己的规则。

这解释了解决问题的顶级处理程序(但只有可以处理异常类型 A 的处理程序)。

IL 本身看起来是有效的:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       49 (0x31)
  .maxstack  1
  IL_0000:  nop
  IL_0001:  nop
  .try
  {
    IL_0002:  nop
    .try
    {
      IL_0003:  nop
      IL_0004:  newobj     instance void A::.ctor()
      IL_0009:  throw
    }  // end .try
    finally
    {
      IL_000a:  nop
      IL_000b:  newobj     instance void B::.ctor()
      IL_0010:  throw
    }  // end handler
  }  // end .try
  catch B 
  {
    IL_0011:  pop
    IL_0012:  nop
    IL_0013:  ldstr      "B"
    IL_0018:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_001d:  nop
    IL_001e:  nop
    IL_001f:  leave.s    IL_0021
  }  // end handler
  IL_0021:  nop
  IL_0022:  nop
  IL_0023:  nop
  IL_0024:  ldstr      "A"
  IL_0029:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_002e:  nop
  IL_002f:  nop
  IL_0030:  ret
} // end of method Program::Main

Mono也有同样的问题http://ideone.com/VVoPx6

【讨论】:

  • 您是否尝试过将代码作为独立的控制台应用程序运行?在我的机器上,它打印“未处理的异常:A:抛出了‘A’类型的异常。”,这绝对不是预期的行为。
  • 不,我得到 Windows 错误报告对话框。如果我说取消,“全部完成!”被打印。您使用的是哪个版本的 .Net、Windows?
  • IL 看起来像是 C# 到 CIL 的直接翻译,但是如果 C# 中 try/catch/finally 的语义(根据规范)与.try/catch/.finally 在 CIL 中,那么 C# 编译器不应该执行如此简单的翻译。不过,这是一个有趣的观点:我应该检查 CIL 规范以了解其内容。
  • 我确实认为您的答案仍然缺少一些相关细节,但仍然决定接受它,因为这是帮助我找到完整答案的一大步。谢谢。
【解决方案2】:

Pavel Krymets 的回答表明,C# 编译器相当直接地将 try/catch/finally 转换为 CIL .try/catch/finally,而 Hans Passant 对我的问题的评论指出了哪里CIL 规范需要当前的行为。所以只要有问题,确实是C#编译器和C#规范的冲突。

我注意到 Roslyn 编译器包含实验性的新语言特性,其中一个新语言特性处理 try/catch:它支持带有 try/catch if 语法的异常过滤器:

try {
  ...
}
catch (Exception e) if (...) {
  ...
}

异常过滤器的一个主要点是它们在任何finally 块之前运行以确定异常是否有任何处理程序。语言规范尚未更新以涵盖这一点:Visual Studio 2015 预览版中包含的语言规范是旧的 C# 5.0 语言规范。但是,即使不是完全不可能,也很难以这样的方式指定其行为,即它会声称finally 块在异常被视为未处理之前执行。鉴于此,我会说相当肯定,不仅当前行为是有意的,而且相当肯定规范将被更新以匹配。

我接受 Pavel Krymets 的回答,因为尽管它本身并不能完全回答我的问题,但这是朝着完整答案迈出的最大一步。

【讨论】:

    猜你喜欢
    • 2011-08-26
    • 2016-09-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-10-17
    • 1970-01-01
    • 2019-03-31
    • 2012-09-26
    相关资源
    最近更新 更多