首页 文章

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

提问于
浏览
17

出现这个问题是因为以前在.NET 4.0中运行的代码在.NET 4.5中出现了未处理的异常,部分原因是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 方法抛出异常(因为它们在using语句中,这些异常碰巧从两个不同的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语句 .

输出:

Propagate, my children
    Throwing CryptographicExecption
    Throwing ArgumentException
    Caught ArgumentException
    Press any key to continue . . .

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

更多信息

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

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

  • 当抛出异常时,包括来自finally块内部的异常,控制转移到封闭的try语句中的第一个catch子句 . 这将继续尝试语句,直到找到合适的语句 .

  • 如果在执行finally块期间抛出异常,并且已经传播了异常, that exception is terminated

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

我确定问题就在这里

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

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

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

问题

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

  • 由于从封闭的finally块抛出 ArgumentException 异常, CryptographicException 如何终止,因为删除 catch (CryptographicException) 块会导致异常无法处理并终止运行时?

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


额外信息编辑

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

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

Propagate, my children
Throwing CryptographicExecption

Unhandled Exception: System.Security.Cryptography.CryptographicException [...]
ram.cs:line 23
Throwing ArgumentException
Caught ArgumentException
Made it out of the exception minefield

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

3 回答

  • 8

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

    一旦throw语句执行,

    • 阶段1就会启动 . CLR正在寻找一个范围内的catch块,它宣告它愿意处理异常 . 在这个阶段,在C#中,没有代码执行 . 从技术上讲,可以执行代码,但该功能不会在C#中公开 .

    找到catch块后,

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

    一旦所有finally块都完成并且堆栈被展开到包含catch语句的方法,

    • stage 3就会启动 . 指令指针设置为catch块中的第一个语句 . 如果此块不包含进一步的throw语句,则执行将在catch块之后的语句处恢复正常 .

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

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

  • 6

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

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

    • CryptographicException 最终被抛入内心 .

    • 外部作用域最终执行,并抛出 ArgumentException . 由于"CryptographicException"在这个时间点是"being propogated",它已经丢失了 .

    • 发生最终捕获,并捕获 ArgumentException .

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

    这正是基于您引用的C#语言规范所发生的情况 . 第一个例外( CryptographicException )实际上消失了 - 它是"lost" .

    但是,你只能通过明确地使用 finally 来达到这种状态,所以我认为你假设're providing the error handling with this expectation or possibility in mind (as you'在那一点上使用了 try ,这意味着你已经接受了你可能有一个例外) .

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

    如果finally块抛出另一个异常,则终止当前异常的处理 .

    第一个例外,在你的情况下 ArgumentException ,基本上"disappears" .

  • 2

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

    事实是,规范是有道理的,而行为则不然 . 当您在较旧的运行时中运行代码时,可以看到更多这样的情况,在该运行时,它的行为与...不同......或至少看起来如此 .

    快速回顾一下

    我在x64 Win7机器上看到了什么:

    • .NET v2.0-3.5 - 抛出 CryptographicException 时的WER对话框 . 点击 Close the program 后,程序继续,好像从未抛出过这样的执行 . 该应用程序是 not terminated . 这是阅读规范时所期望的行为,以及在.NET中实现异常处理的is well defined by the architects .

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

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

    这绝不是你所期待的

    谁会怀疑 Just-In-Time debugger

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

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

    由于jit调试器终止了应用程序,它使它成为 seem ,就像它不符合规范所说的那样,实际上它确实如此 .

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

    Running in .NET 4.0


    事实证明,有一种解决方法,但由于您的应用程序正在终止,因此没有理由解决此问题 .

    • 弹出Just-In-Time调试器窗口时,选中 Manually choose the debugging engines 并单击是,即可调试 .

    • 当Visual Studio为您提供引擎选项时,单击取消 .

    • 这将导致程序继续,或者弹出WER对话框,具体取决于您的机器配置 . 如果发生这种情况,告诉它关闭程序实际上不会关闭它,它将继续运行,好像一切都好 .

相关问题