【问题标题】:Catch block is not being evaluated when exceptions are thrown from finallys从 finally 抛出异常时,未评估 Catch 块
【发布时间】:2012-08-22 11:21: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 块中抛出的)。当cryptoStream.Dispose()StreamReader 调用时,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 语句。

输出:

传播,我的孩子们 抛出 CryptographicExecption 抛出参数异常 捕获的参数异常 按任意键继续 。 . .

似乎从未执行过CryptographicException catch 块。但是,删除该 catch 块会导致异常终止运行时。

更多信息

编辑:这已更新为规范的最新版本。我碰巧从 MSDN 上抢到的那个有更旧的措辞。 Lost 已更新为 terminated

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

  • 当引发异常时,包括从 finally 块内部引发的异常,控制将转移到封闭 try 语句中的第一个 catch 子句。这会继续尝试语句,直到找到合适的语句。
  • 如果在 finally 块的执行过程中抛出异常,并且异常已经被传播,该异常被终止

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

我确定问题就在这里

在大多数情况下,很容易可视化运行时在做什么。代码执行到抛出异常的第一个 finally 块 (F1)。随着异常的传播,第二个异常从第二个 finally 块 (F2) 中抛出。

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

这里是模糊不清的地方:规范说第一个异常将被终止。但是,如果从代码中删除第二个 catch 块 (C2),则应该丢失的 CryptographicException 现在是一个未处理的异常,会终止程序。在C2 存在的情况下,代码不会因未处理的异常而终止,因此表面上它似乎正在处理异常,但块中的实际异常处理代码永远不会执行。

问题

问题基本相同,但为了具体而重新措辞。

  1. CryptographicException 因封闭 finally 块引发的 ArgumentException 异常而终止,因为删除 catch (CryptographicException) 块会导致异常未处理并终止运行时?

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


额外信息编辑

我仍在调查此问题的实际行为,其中许多答案对至少回答上述部分问题特别有帮助。

另一个奇怪的行为是 .NET 4.5 和 .NET 3.5 之间的差异,当您运行带有 catch (CryptographicException) 块注释的代码时会发生这种情况。 .NET 4.5 将抛出 CryptographicException 并终止应用程序。然而,.NET 3.5 的行为似乎更符合异常所在的 C# 规范。

传播,我的孩子们 抛出 CryptographicExecption 未处理的异常:System.Security.Cryptography.CryptographicException [...] ram.cs:第 23 行 抛出参数异常 捕获的参数异常 走出了异常雷区

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

【问题讨论】:

  • 您的代码准确地展示了语言规范告诉您的内容。如果你用“replaced”代替“lost”也许会有所帮助。
  • @HansPassant - 我在上面发布的精简代码以及连接问题链接中的实际代码都表明异常丢失了,但只有当我尝试捕获它时.当CryptographicException 的catch 块被删除时,该异常将未处理并终止应用程序,因此它永远不会丢失或替换。如果存在 catch 块,应用程序会成功,但代码不会执行。
  • finally 块在确定要继续的地方之前不会执行。如果找不到 catch 子句,那将无处可去。
  • @HansPassant - 我不确定我是否跟随。 finally 块执行,因为它们所做的只是抛出异常。加密 catch 块没有执行,我觉得这很令人困惑,因为运行时的行为就像 catch 的存在正在处理该异常。如果删除它,运行时会以未处理的异常终止,处理ArgumentNullException 之后。我不确定我对自己的解释有多好。如果您有 .NET 4.5,我鼓励您测试我在上面发布的代码,甚至是顶部链接的连接问题中的代码,因为它会更好地解释它。
  • @samuelneff - 我希望没有人会这样做,但是当图书馆不遵循指导方针并在处理时抛出异常时,它发生的数量令人惊讶。 CryptoStream 是执行此操作的基类库的示例,也是此问题所基于的。

标签: c# .net specifications .net-4.5


【解决方案1】:

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

  • 只要 throw 语句执行,阶段 1 就会启动。 CLR 去寻找一个范围内的 catch 块,该块表明它愿意处理异常。在这个阶段,在 C# 中,没有代码执行。从技术上讲,可以执行代码,但 C# 中没有公开该功能。

  • 一旦找到 catch 块并且 CLR 知道从哪里恢复执行,阶段 2 就会开始。然后它可以可靠地确定需要执行哪些 finally 块。任何方法堆栈帧也会展开。

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

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

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

【讨论】:

  • 您能否解释一下,如果 finally 块中的异常抛出操作被包装到 try..catch 中会发生什么?异常处理序列会继续并成功处理原始异常吗??
  • 您可以在上面的回答中看到我的意思。我要放弃原来的例外吗?
  • @KonstantinVasilcov - 很容易找到。 A:不,原件没有丢失。
【解决方案2】:

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

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

  • CryptographicException 最终被扔进内部。
  • 外部作用域最终执行,并抛出ArgumentException。由于此时“CryptographicException”正在“传播”,因此它丢失了。
  • 最终捕获发生,ArgumentException 被捕获。

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

根据您引用的 C# 语言规范,这正是发生的情况。第一个异常(CryptographicException)实际上消失了——它“丢失了”。

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

这在8.9.5的规范中基本有详细解释(你引用的8.10中的文字是指本节):

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

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

【讨论】:

  • 如果第一个异常离开了捕获,您也可以使用try/catch/finally 达到此状态。
  • @BAF 是的 - 我更想说的是,您必须尝试一下 finally(您也可以在其中遇到问题)。尝试/捕获或缺少任何“尝试”是不够的。我改写了 - 是不是更清楚?
  • 啊,好吧。我想当时的措辞让我有点困惑;好像你在说发生这种情况的唯一方法是使用try/finally
  • 你回答的第二部分,是我想要回答的真正问题。我预计,由于它的措辞方式,该异常会消失。问题是它并没有消失。或者至少不是严格意义上的。 catch 块中的代码永远不会执行。但是,如果您删除 catch 块,应用程序将失败并出现未处理的异常。它似乎只会迷路,你试着抓住它。
【解决方案3】:

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

事实上,规范是有道理的,而行为却不是。当您在较旧的运行时运行代码时,这种情况更加明显,它的行为完全不同......或至少看起来

快速回顾

我在 x64 Win7 机器上看到的:

  • .NET v2.0-3.5 - 抛出 CryptographicException 时出现 WER 对话框。在点击Close the program 后,程序继续运行,就好像从未抛出过执行。应用程序未终止。这是人们阅读规范所期望的行为,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 来禁用 vsjitdebugger 在失败时启动。果然,应用程序忽略了异常并继续运行,就像 .NET 2.0 一样。


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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-10-03
    • 2021-09-09
    • 2017-10-03
    • 1970-01-01
    相关资源
    最近更新 更多