21

拿这个代码:

using System;

namespace OddThrow
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                throw new Exception("Exception!");
            }
            finally
            {
                System.Threading.Thread.Sleep(2500);
                Console.Error.WriteLine("I'm dying!");
                System.Threading.Thread.Sleep(2500);
            }
        }
    }
}

这给了我这个输出:

Unhandled Exception: System.Exception: Exception!
   at OddThrow.Program.Main(String[] args) in C:\Documents and Settings\username
\My Documents\Visual Studio 2008\Projects\OddThrow\OddThrow\Program.cs:line 14
I'm dying!

我的问题是:为什么未处理的异常文本出现在 finally 之前?在我看来,finally 应该在堆栈展开时被执行,甚至在我们知道这个异常未处理之前。注意对 Sleep() 的调用 - 这些发生打印未处理的异常之后,就好像它正在执行以下操作一样:

  1. 未处理的异常文本/消息
  2. 最后阻塞。
  3. 终止申请

根据 C# 标准,§8.9.5,这种行为是错误的:

  • 在当前函数成员中,检查包含抛出点的每个 try 语句。对于每个语句 S,从最里面的 try 语句开始,到最外面的 try 语句结束,评估以下步骤:
    • 如果 S 的 try 块包含了抛出点,并且如果 S 有一个或多个 catch 子句,则按出现的顺序检查 catch 子句以找到合适的异常处理程序。指定异常类型或异常类型的基类型的第一个 catch 子句被视为匹配项。一般的 catch 子句(第 8.10 节)被认为是任何异常类型的匹配项。如果找到匹配的 catch 子句,则通过将控制转移到该 catch 子句的块来完成异常传播。
    • 否则,如果 S 的 try 块或 catch 块包围了抛出点,并且如果 S 有 finally 块,则控制转移到 finally 块。如果 finally 块抛出另一个异常,则终止当前异常的处理。否则,当控制到达 finally 块的终点时,继续处理当前异常。
  • 如果异常处理程序不在当前函数成员调用中,则终止函数成员调用。然后对函数成员的调用者重复上述步骤,并使用与调用函数成员的语句相对应的抛出点。
  • 如果异常处理终止了当前线程中的所有函数成员调用,表明该线程没有异常处理程序,则该线程本身终止。这种终止的影响是实现定义的。

我哪里错了?(我有一些自定义控制台错误消息,这是在路上。轻微的,只是烦人,让我质疑语言......)

4

9 回答 9

7

该标准关于执行顺序的陈述是正确的,并且与您所观察的内容不一致。“未处理的异常”消息被允许出现在进程中的任何点,因为它只是来自 CLR 的消息,而不是真正的异常处理程序本身。关于执行顺序的规则仅适用于在 CLR 内部执行的代码,而不适用于 CLR 本身所做的事情。

您实际上所做的是公开一个实现细节,即通过查看我们在其中的 try{} 块的堆栈来识别未处理的异常,而不是通过实际探索到根。通过查看此堆栈可能会或可能不会处理异常,但是以这种方式识别未处理的异常。

您可能知道,如果您在主函数中放置一个顶级 try{}catch{},那么您将看到您所期望的行为:每个函数的 finally 将在检查下一帧是否匹配 catch{之前执行}。

于 2009-05-19T20:53:11.727 回答
2

我阅读内容的方式可能会偏离基础……但是您可以尝试 finally 块而不会遇到问题。

从您发布的描述来看,由于 Exception 从未被捕获,它会冒泡到调用者,通过堆栈向上工作,最终未处理,调用终止,然后调用 finally 块。

于 2009-05-19T20:35:36.350 回答
2

输出实际上来自默认的 CLR 异常处理程序。异常处理程序发生在 finally 块之前。在 finally 块之后,CLR 由于未处理的异常而终止(它之前不能终止,因为 c# 保证 [1] 调用 finally 子句)。

所以我想说这只是标准行为,异常处理发生在 finally 之前。

[1] 在正常运行期间保证至少在没有内部运行时错误或断电的情况下

于 2009-05-19T20:36:18.017 回答
2

要添加更多内容,请考虑以下内容:

using System;
namespace OddThrow
{
    class Program
    {
        static void Main()
        {
            AppDomain.CurrentDomain.UnhandledException +=
                delegate(object sender, UnhandledExceptionEventArgs e)
            {
                Console.Out.WriteLine("In AppDomain.UnhandledException");
            };
            try
            {
                throw new Exception("Exception!");
            }
            catch
            {
                Console.Error.WriteLine("In catch");
                throw;
            }
            finally
            {
                Console.Error.WriteLine("In finally");
            }
        }
    }
}

在我的系统(挪威语)上显示了这一点:

[C:\..] ConsoleApplication5.exe
In catch
In AppDomain.UnhandledException

Ubehandlet unntak: System.Exception: Exception!
   ved OddThrow.Program.Main() i ..\Program.cs:linje 24
In finally
于 2009-05-19T20:49:40.563 回答
1

尽管没有完全预期,但该程序确实按应有的方式运行。finally 块不希望首先运行,它只希望始终运行。

我调整了你的样本:

public static void Main()
{
    try
    {
        Console.WriteLine("Before throwing");
        throw new Exception("Exception!");
    }
    finally
    {
        Console.WriteLine("In finally");
        Console.ReadLine();
    }
}

在这种情况下,您将看到令人讨厌的未处理异常对话框,但随后控制台将输出并等待输入,从而执行 finally,而不是在 windows 本身捕获未处理异常之前。

于 2009-05-19T20:37:33.617 回答
0

如果 try-catch-finally 块在某个时间点被捕获,它们的工作方式与您预期的完全一样。当我为此编写一个测试程序并使用各种嵌套级别时,它的行为方式与您所描述的匹配的唯一情况是异常完全未被代码处理,并且它冒泡到操作系统。

每次我运行它时,操作系统都是创建错误消息的原因。所以问题不在于 C#,而是因为用户代码未处理的错误不再受应用程序的控制,因此运行时(我相信)不能强制执行模式。

如果您创建了一个 Windows 窗体应用程序,并将所有消息写入文本框(然后立即刷新它们)而不是直接写入控制台,那么您根本不会看到该错误消息,因为它已插入错误控制台由调用应用程序而不是您自己的代码。

编辑

我将尝试强调其中的关键部分。未处理的异常不在您的控制范围内,您无法确定何时执行它们的异常处理程序。如果您在应用程序中的某个时刻finally捕获到异常,则这些块将在堆栈中较低的块之前执行catch

于 2009-05-19T20:29:46.797 回答
0

没有 catch 的 try/finally 将使用默认处理程序,该处理程序与您看到的完全相同。我一直都在使用它,例如,在处理异常会覆盖错误但您仍然想做一些清理工作的情况下。

还要记住,标准错误的输出和标准输出是缓冲的。

于 2009-05-19T20:37:01.247 回答
0

将几个答案放在一起,发生的情况是,一旦您有一个未处理的异常,就会在 AppDomain 上引发一个 UnhandledExceptionEvent,然后代码继续执行(即 finally)。这是有关该事件的 MSDN 文章

于 2009-05-19T21:02:43.860 回答
0

下次尝试:

  1. 我相信 c# 标准中没有提到这种情况,我同意它似乎几乎与它相矛盾。

  2. 我相信发生这种情况的内部原因有点像这样:当您的代码中有更多捕获时,CLR 将其默认异常处理程序作为 SEH 处理程序注册到 FS:[0] 中,这些处理程序被添加到 SEH 链中。或者,在 SEH 处理期间只调用 CLR 处理程序并在内部处理 CLR 异常链,我不知道是哪个。

在引发异常时的代码中,只有默认处理程序位于 SEH 链中。在任何堆栈展开开始之前调用此处理程序。

默认异常处理程序知道堆栈上没有注册异常处理程序。因此,它首先调用所有已注册的 UnhandledException 处理程序,然后打印其错误消息并将 AppDomain 标记为卸载。

只有在该堆栈展开甚至开始并且根据 c# 标准调用 finally 块之后。

如我所见,c# 标准中没有考虑 CLR 处理未处理异常的方式,只考虑堆栈展开期间调用 finally 的顺序。保留此顺序。之后,“这种终止的影响是实现定义的”。条款生效。

于 2009-05-19T21:09:34.457 回答