将未处理的异常更改为finally块中的已处理异常

时间:2014-11-19 13:24:05

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

考虑这个程序:

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语句结束,评估以下步骤:      
        
    • 如果try的{​​{1}}块包含抛出点且S有一个或多个S子句,则检查catch子句[...]
    •   
    • 否则,如果catch阻止或try阻止catch阻止投掷点并且SS阻止,则转移控制权到finally区块。如果finally块抛出另一个异常,则终止当前异常的处理。否则,当控制到达finally块的结束点时,继续处理当前异常。
    •   
  •   
  • 如果当前函数调用中未找到异常处理程序,则终止函数调用,并发生以下任一情况:      
        
    • [...]
    •   
  •   
  • 如果异常处理终止当前线程中的所有函数成员调用,指示该线程没有该异常的处理程序,则该线程本身终止。此类终止的影响是实现定义的。
  •   

根据我对规范的解读,异常不被认为是未处理的,直到之后所有函数调用都被终止,并且函数调用不会被终止,直到{{ 1}}处理程序已执行。

我在这里遗漏了什么,或者微软的C#实现是否与他们自己的规范不一致?

2 个答案:

答案 0 :(得分: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

答案 1 :(得分:1)

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 Preview中包含的语言规范是旧的C#5.0语言规范。但是,如果不是完全不可能指定行为,那么在认为异常被认为未处理之前它将声称finally块被执行将是困难的。鉴于此,我说它相当肯定不仅当前的行为是有意的,而且还相当确定规范将被更新以匹配。

我接受了Pavel Krymets的回答,因为即使它没有完全回答我的问题,这是迈向完整答案的最大一步。