代码之家  ›  专栏  ›  技术社区  ›  Christopher Currens

当从finallys抛出异常时,Catch块未被求值

  •  17
  • Christopher Currens  · 技术社区  · 12 年前

    出现此问题是因为以前在.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))
    

    这个问题是 Dispose CryptoStream的方法(因为它们在using语句中,所以这些异常恰好是从两个不同的finally块抛出的)。什么时候 cryptoStream.Dispose() 由调用 StreamReader 这个 CryptographicException 被抛出。第二次 cryptoStream.Dispose() 被调用,在其using语句中,它抛出 ArgumentNullException

    下面的代码从上面提供的链接中删除了大部分不必要的代码,并将using语句展开到try/finally中,以清楚地表明它们是在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 . . .
    

    似乎 加密异常 catch块始终执行。但是,删除catch块会导致异常终止运行时。

    更多信息

    编辑:已更新为规范的最新版本。我碰巧从MSDN上抢来的那本书的措辞比较陈旧。 Lost 已更新为 terminated

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

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

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

    我确信问题就在这里的某个地方

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

    根据规范 加密异常 从中抛出 F1 现在已终止,运行时正在为 ArgumentException 。运行时找到一个处理程序,并执行 争论异常 ( C1 )。

    这就是它变得模糊的地方:规范说第一个异常将被终止。但是,如果第二个挡块( C2 )从代码中删除,则 加密异常 本来应该丢失的,现在是一个未处理的异常,终止了程序。与 C2 目前,代码不会因未处理的异常而终止,因此从表面上看 出现 处理异常,但块中的实际异常处理代码从未执行。

    问题

    这些问题基本上是一样的,但为了具体起见,重新措辞。

    1. 为什么 加密异常 由于 争论异常 从封闭的finally块引发的异常,如删除 catch (CryptographicException) 块导致异常无法处理并终止运行时?

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


    附加信息编辑

    我仍在研究这方面的实际行为,许多答案至少在回答上述问题的部分方面特别有帮助。

    另一个奇怪的行为,当您使用 catch(加密异常) 块被注释掉,是.NET 4.5和.NET 3.5之间的区别。NET 4.5将抛出 加密异常 并终止应用程序。然而,NET3.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
    

    在.NET3.5中,我看到了我在规范中看到的内容。异常变为“丢失”或“终止”,因为唯一需要捕获的是 争论异常 正因为如此,程序才能继续执行。我的机器上只有.NET 4.5,我想知道在.NET 4.0中是否会发生这种情况?

    3 回复  |  直到 4 年前
        1
  •  8
  •   Hans Passant    12 年前

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

    • 一旦执行了throw语句,阶段1就启动了。CLR在范围内寻找一个catch块,该块通告它愿意处理异常。在C#中, 没有代码执行 从技术上讲,执行代码是可能的,但这种能力在C#中没有公开。

    • 一旦catch块被定位并且CLR知道在哪里恢复执行,阶段2就开始了。然后,它可以可靠地确定需要执行哪些最终块。任何方法堆栈帧也将展开。

    • 当所有finally块都完成并且堆栈被展开到包含catch语句的方法时,阶段3就开始了。指令指针设置为catch块中的第一条语句。如果该块不包含其他throw语句,则在catch块之后的语句处恢复正常执行。

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

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

        2
  •  6
  •   Reed Copsey    12 年前

    如果在finally块的执行过程中抛出异常,并且已经在传播异常,则该异常将丢失

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

    • CryptographicException 最后被扔进了内心。
    • 外部作用域最终执行,并抛出 ArgumentException 。由于“CryptographicException”在此时“正在被提议”,因此它已丢失。
    • 最终捕获发生,并且 争论异常 被捕获。

    …而且第一个异常简单地消失在以太中是没有意义的,因为还有另一个异常从不同的finally块抛出。

    这正是基于您引用的C#语言规范所发生的事情。第一个例外( 加密异常 )实际上消失了——它“消失了”。

    您只能通过显式使用 finally 不过,我认为假设您在提供错误处理时考虑到了这种期望或可能性(正如您使用的 try 在这一点上,这意味着你已经接受了你可能有一个例外)。

    这基本上在中的规范中进行了详细解释 8.9.5 (中的文本 8.10 您引用的内容指的是本节):

    如果finally块引发另一个异常,则终止对当前异常的处理。

    第一个例外,在您的情况下 争论异常 ,基本上“消失”了。

        3
  •  2
  •   Christopher Currens    12 年前

    事实证明,我 疯子根据我对这个问题的回答,我认为我似乎很难理解规范中如此清晰地概述了什么。这真的一点也不难理解。

    事实是,规范是有道理的,而行为则不然。当您在较旧的运行时中运行代码时,情况更是如此 彻底地 不同的或者至少 出现

    简要回顾

    我在x64 Win7机器上看到的:

    • .NET v2.0-3.5-WER对话框时 CryptographicException 被抛出。击打后 Close the program ,程序继续,就好像从未抛出过exeception一样。应用程序是 未终止 。这是人们在阅读规范时所期望的行为,并且 is well defined by the architects 谁在.NET中实现了异常处理。

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

    事实证明,几乎任何试图回答我的问题的人都会得到与我完全相同的结果,所以这就解释了为什么没有人能回答我为什么运行时会因其吞下的异常而终止的问题。

    这从来都不是你所期望的

    谁会怀疑 实时调试器 ?

    您可能已经注意到,在.NET 2下运行应用程序会产生与.NET 4不同的错误对话框。然而,如果你和我一样,在开发周期中你已经开始期待这个窗口,所以你什么都没想。

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

    由于jit调试器终止了应用程序 似乎 就像它不符合规范所说的,而事实上,它符合规范。

    为了测试这一点,我通过更改位于 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug\Auto 从1到0。果不其然,应用程序忽略了异常并继续,就像.NET 2.0一样。

    Running in .NET 4.0


    事实证明,有一个解决方法,尽管实际上没有理由解决这种行为,因为您的应用程序正在终止。

    1. 当“实时调试器”窗口弹出时,检查 Manually choose the debugging engines 然后单击要调试的“是”。
    2. 当Visual Studio为您提供引擎选项时,请单击“取消”。
    3. 这将导致程序继续运行,或者根据您的机器配置弹出WER对话框。如果发生这种情况,告诉它关闭程序实际上并不会关闭它,它将继续运行,就好像一切正常一样。