当从finallys抛出exception时,不会评估Catch块

出现这个问题是因为以前在.NET 4.0中运行的代码在.NET 4.5中出现了未处理的exception,部分原因是try / finallys。 如果您需要详细信息,请阅读Microsoft connect 。 我用它作为这个例子的基础,所以它可能有助于引用。

代码

对于那些选择不阅读这个问题背后细节的人来说,这里可以快速了解发生这种情况的条件:

using(var ms = new MemoryStream(encryptedData)) using(var cryptoStream = new CryptoStream(encryptedData, decryptor, CryptoStreamMode.Read)) using(var sr = new StreamReader(cryptoStream)) 

这个问题是从CryptoStream的Dispose方法抛出exception(因为它们在using语句中,这些exception碰巧是从两个不同的finally块抛出的)。 当StreamReader调用cryptoStream.Dispose() ,将抛出CryptographicException 。 第二次cryptoStream.Dispose() ,在其using语句中,它抛出一个ArgumentNullException

下面的代码从上面提供的链接中删除了大部分不必要的代码,并将using语句展开到try / finallys中,以清楚地表明它们正在抛出finally块。

 using System; using System.Security.Cryptography; namespace Sandbox { public class Program { public static void Main(string[] args) { try { try { try { Console.WriteLine("Propagate, my children"); } finally { // F1 Console.WriteLine("Throwing CryptographicExecption"); throw new CryptographicException(); } } finally { // F2 Console.WriteLine("Throwing ArgumentException"); throw new ArgumentException(); } } catch (ArgumentException) { // C1 Console.WriteLine("Caught ArgumentException"); } // Same behavior if this was in an enclosing try/catch catch (CryptographicException) { // C2 Console.WriteLine("Caught CryptographicException"); } Console.WriteLine("Made it out of the exception minefield"); } }} 

注意:try / finally对应于引用代码中的扩展using语句。

输出:

    传播,我的孩子们
    投掷密码执行
    抛出ArgumentException
    捕获ArgumentException
    按任意键继续 。  。  。

似乎没有执行CryptographicException catch块。 但是,删除该catch块会导致exception终止运行时。

更多信息

编辑:这已更新为规范的最新版本。 我碰巧抓住MSDN的那个用了较旧的措辞。 Lost已更新为已terminated

深入了解C#规范,第8.9.5节和第8.10节讨论了exception行为:

  • 抛出exception(包括从finally块内部)时,控制将转移到封闭的try语句中的第一个catch子句。 这将继续尝试语句,直到找到合适的语句。
  • 如果在执行finally块期间抛出exception,并且已经传播了exception,则终止该exception

“终止”使得看起来第一个exception将永远被第二个抛出的exception隐藏,尽管它似乎不是正在发生的事情。

我确定问题就在这里

在大多数情况下,可以很容易地看到运行时正在做什么。 代码执行到第一个finally块( F1 ),其中抛出exception。 当exception传播时,从第二个finally块( F2 )抛出第二个exception。

根据规范,从F1抛出的CryptographicException现在终止,运行时正在寻找ArgumentException的处理程序。 运行时找到一个处理程序,并在catch块中为ArgumentExceptionC1 )执行代码。

这里有雾的地方:规范说第一个例外将被终止。 但是,如果从代码中删除了第二个catch块( C2 ),则假定丢失的CryptographicException现在是一个未处理的exception,它终止了该程序。 在存在C2 ,代码不会从未处理的exception终止,因此从表面看它似乎正在处理exception,但是块中的实际exception处理代码永远不会被执行。

问题

问题基本相同,但针对特异性进行了重新措辞。

  1. 由于从封闭的finally块抛出ArgumentExceptionexception, CryptographicException是如何终止的,因为删除catch (CryptographicException)块导致exception无法处理并终止运行时?

  2. 由于运行时似乎在catch (CryptographicException)块存在时处理CryptographicException ,为什么块内的代码没有执行?


额外的信息编辑

我仍然在研究这个问题的实际行为,并且许多答案在至少回答上述问题的部分内容时特别有帮助。

另一个奇怪的行为是在运行带有catch (CryptographicException)块的代码时发生的,这是.NET 4.5和.NET 3.5之间的区别。 .NET 4.5将抛出CryptographicException并终止应用程序。 但是,.NET 3.5似乎表现得更符合C#规范所在的exception。

传播,我的孩子们
投掷密码执行

未处理的exception:System.Security.Cryptography.CryptographicException [...]
 ram.cs:第23行
抛出ArgumentException
捕获ArgumentException
从exception雷区中脱颖而出

在.NET 3.5中,我看到了我在规范中读到的内容。 exception变为“丢失”或“终止”,因为唯一需要捕获的是ArgumentException 。 因此,程序可以继续执行。 我的机器上只有.NET 4.5,我想知道这是否发生在.NET 4.0中?

.NET中的exception处理有3个不同的阶段:

  • 一旦throw语句执行,第1阶段就会启动。 CLR寻找一个范围内的catch块,该范围宣告它愿意处理exception。 在这个阶段,在C#中, 没有代码执行 。 从技术上讲,可以执行代码但该function不会在C#中公开。

  • 找到catch块后,阶段2开始,CLR知道恢复执行的位置。 然后,它可以可靠地确定最终需要执行的块。 任何方法堆栈帧也都被解开。

  • 一旦所有finally块完成并且堆栈被展开到包含catch语句的方法,则阶段3开始。 指令指针设置为catch块中的第一个语句。 如果此块不包含进一步的throw语句,则执行将在catch块之后的语句处恢复正常。

因此,代码片段中的核心要求是范围内存在catch(CryptographicException)。 没有它,第1阶段失败,CLR不知道如何恢复执行。 线程已经死了,通常也会根据exception处理策略终止程序。 finally块中没有一个会执行。

如果在阶段2中,finally块抛出exception,则立即中断正常的exception处理序列。 最初的例外是“丢失”,它永远不会进入第3阶段,因此无法在您的程序中观察到。 exception处理从阶段1开始,现在寻找新的exception并从该finally块的范围开始。

如果在执行finally块期间抛出exception,并且已经传播了exception,则该exception将丢失

基本上,执行时会发生什么:

  • 最后在内部抛出CryptographicException
  • 外部作用域最终执行,并抛出ArgumentException 。 由于“CryptographicException”在这个时间点被“传播”,它就会丢失。
  • 发生最终捕获,并捕获ArgumentException

…并且第一个例外消失在以太中是没有意义的,只是因为从另一个finally块抛出了另一个exception。

这正是基于您引用的C#语言规范所发生的情况。 第一个exception( CryptographicException )实际上消失了 – 它“丢失”了。

但是,你只能通过显式使用finally来达到这种状态,所以我相信你假设你提供error handling时考虑到了这种期望或可能性(因为你在那时使用了try ,这意味着你已经接受你可能有例外)。

这基本上在8.9.5中的规范中有详细解释(您引用的8.10的文字引用了本节):

如果finally块抛出另一个exception,则终止当前exception的处理。

第一个例外,在你的情况下ArgumentException ,基本上“消失”。

事实certificate,我并不疯狂。 基于我得到这个问题的答案,我认为我似乎很难理解规范中如此清晰概述的内容。 这根本不是很难掌握。

事实是,规范是有道理的,而行为则不然。 当您在较旧的运行时中运行代码时,情况会更加如此,在该运行时,它的行为完全不同……或者至少看起来如此

快速回顾一下

我在x64 Win7机器上看到的:

  • .NET v2.0-3.5 – 抛出CryptographicException时的WER对话框。 点击Close the program ,程序继续执行,就像从未抛出执行一样。 申请未终止 。 这是阅读规范时所期望的行为,并且由在.NET中实现exception处理的架构师很好地定义 。

  • .NET v4.0-4.5 – 不显示WER对话框。 而是会出现一个窗口,询问您是否要调试该程序。 单击no会导致程序立即终止。 之后没有执行finally块。

事实certificate,几乎所有试图回答我问题的人都会得到与我相同的结果,这就解释了为什么没人能回答我为什么运行时终止于它吞下的exception的问题。

它永远不会像你期望的那样

谁会怀疑Just-In-Time调试器

你可能已经注意到在.NET 2下运行应用程序会产生一个与.NET 4不同的错误对话框。但是,如果你像我一样,你会在开发周期中期望这个窗口,所以你没有想一想。

vsjitdebugger可执行文件强制终止应用程序,而不是让它继续。 在2.0运行时, dw20.exe没有这种行为,事实上,你看到的第一件事是WER消息。

由于jit调试器终止了应用程序,它使得它似乎不符合规范所说的,事实上它确实如此。

为了测试这一点,我通过将HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug\Auto的注册表项从1更改为0来禁用启动失败的HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug\Auto果然,应用程序忽略了exception并继续,就像.NET 2.0一样。

Running in .NET 4.0


事实certificate,有一种解决方法,但实际上没有理由解决此问题,因为您的应用程序正在终止。

  1. 弹出Just-In-Time调试器窗口时,选中Manually choose the debugging engines ,然后单击是,您要调试。
  2. 当Visual Studio为您提供引擎选项时,单击取消。
  3. 这将导致程序继续,或者弹出WER对话框,具体取决于您的机器配置。 如果发生这种情况,告诉它关闭程序将不会实际关闭它,它将继续运行,好像一切都好。