为什么.NET异常没有被try / catch块捕获?

时间:2020-03-05 18:45:27  来源:igfitidea点击:

我正在使用ANTLR解析器库进行C#开发的项目。我已经建立了一种语法来解析一些文本,并且效果很好。但是,当解析器遇到非法或者意外的令牌时,它将引发许多异常之一。问题在于,在某些情况下(不是全部),我的try / catch块无法捕获它,而是将其作为未处理的异常停止执行。

对我来说,问题是,除了完整的代码,我无法在其他任何地方复制此问题。调用堆栈显示异常肯定在我的try / catch(Exception)块内发生。我唯一能想到的是,在我的代码和引发异常的代码之间发生了一些ANTLR程序集调用,并且该库未启用调试功能,因此我无法逐步进行调试。我想知道不可反调试的程序集是否会阻止异常冒泡?调用堆栈如下所示:外部程序集调用在Antlr.Runtime中:

Expl.Itinerary.dll!TimeDefLexer.mTokens() Line 1213 C#
    Antlr3.Runtime.dll!Antlr.Runtime.Lexer.NextToken() + 0xfc bytes 
    Antlr3.Runtime.dll!Antlr.Runtime.CommonTokenStream.FillBuffer() + 0x22c bytes   
    Antlr3.Runtime.dll!Antlr.Runtime.CommonTokenStream.LT(int k = 1) + 0x68 bytes
    Expl.Itinerary.dll!TimeDefParser.prog() Line 109 + 0x17 bytes   C#
    Expl.Itinerary.dll!Expl.Itinerary.TDLParser.Parse(string Text = "", Expl.Itinerary.IItinerary Itinerary = {Expl.Itinerary.MemoryItinerary}) Line 17 + 0xa bytes C#

来自Parse()最底部调用的代码片段如下所示:

try {
        // Execution stopped at parser.prog()
        TimeDefParser.prog_return prog_ret = parser.prog();
        return prog_ret == null ? null : prog_ret.value;
     }
     catch (Exception ex) {
        throw new ParserException(ex.Message, ex);
     }

对我来说,catch(Exception)子句应该捕获任何异常。有什么理由不这样做吗?

更新:我使用Reflector跟踪了外部程序集,没有发现任何线程化的迹象。该程序集似乎只是ANTLR生成的代码的运行时实用程序类。从TimeDefLexer.mTokens()方法引发的异常,其类型为NoViableAltException,该异常派生自RecognitionException-> Exception。当词法分析器无法理解流中的下一个标记时,抛出此异常。换句话说,输入无效。应该发生此异常,但是我的try / catch块应该捕获了此异常。

同样,重新抛出ParserException与这种情况确实无关。那是一个抽象层,它在解析期间接受任何异常并将其转换为我自己的ParserException。我遇到的异常处理问题从未达到那行代码。实际上,我注释掉了"引发新的ParserException"部分,并且仍然收到了相同的结果。

还有一件事,我修改了有问题的原始try / catch块以捕获NoViableAltException,从而消除了任何继承混乱。我仍然收到相同的结果。

曾经有人建议有时在调试模式下,VS在捕获处理的异常时会过于活跃,但是在发布模式下也会发生此问题。

伙计,我还是很困惑!我之前没有提到过,但是我正在运行VS 2008,我的所有代码都是3.5. 外部组件为2.0。另外,我的一些代码在2.0程序集中将一个类作为子类。版本不匹配会导致此问题吗?

更新2:通过将.NET 3.5代码的相关部分移植到.NET 2.0项目并复制相同的方案,我能够消除.NET版本冲突。在.NET 2.0中一致运行时,我能够复制相同的未处理异常。

我了解到ANTLR最近发布了3.1. 因此,我从3.0.1升级并重试。事实证明,生成的代码经过了一些重构,但是在我的测试用例中也发生了同样的未处理异常。

更新3:
我已经在一个简化的VS 2008项目中复制了此方案。随意自己下载和检查项目。我已经应用了所有很棒的建议,但是还没有克服这个障碍。

如果可以找到解决方法,请分享发现。再次感谢!

谢谢,但是VS 2008会在未处理的异常时自动中断。另外,我没有Debug-> Exceptions对话框。引发的NoViableAltException是完全有意的,旨在被用户代码捕获。由于未按预期捕获它,因此程序执行作为未处理的异常意外中止。

引发的异常是从Exception派生的,ANTLR没有多线程处理。

解决方案

回答

是否有可能在另一个线程中引发了异常?显然,调用代码是单线程的,但也许我们正在使用的库在后台进行了一些多线程操作。

回答

不管程序集是否已被编译为发行版,异常都应该"冒泡"给调用者,没有理由不在调试模式下进行编译的程序集对此没有任何影响。

我同意Daniel的建议,即异常可能发生在单独的线程上,请尝试将线程异常事件挂接到Application.ThreadException中。当发生任何未处理的线程异常时,应引发此事件。我们可以这样修改代码:

using System.Threading;

...

void Application_ThreadException(object sender, ThreadExceptionEventArgs e) {
  throw new ParserException(e.Exception.Message, e.Exception);
}    

 ...

 var exceptionHandler = 
    new ThreadExceptionEventHandler(Application_ThreadException);
 Application.ThreadException += exceptionHandler;
 try {
    // Execution stopped at parser.prog()
    TimeDefParser.prog_return prog_ret = parser.prog();
    return prog_ret == null ? null : prog_ret.value;
 }
 catch (Exception ex) {
    throw new ParserException(ex.Message, ex);
 }
 finally {
    Application.ThreadException -= exceptionHandler;
 }

回答

我们可以将VS.Net设置为在发生任何异常时立即中断。只需在调试模式下运行项目,它将在引发异常后立即停止。然后,我们应该对为什么不被捕获有一个更好的了解。

另外,我们可以放入一些代码来捕获所有未处理的异常。阅读链接以获取更多信息,但基础是这两行。

Application.ThreadException += new ThreadExceptionEventHandler(ThreadExceptionHandler);

 // Catch all unhandled exceptions in all threads.
 AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(UnhandledExceptionHandler);

回答

哦,参考了基贝说的话;如果在VS中选择Debug | Exceptions并仅单击" throw"列中的所有框,则应将AFAIK中的所有内容都选为"第一个机会异常",即VS将指示异常何时将由其他所有事物处理,并且中断相关代码。这应该有助于调试。

回答

"Also, you can put some code in to
  catch all unhandled exceptions. Read
  the link for more info, but the basics
  are these two lines."

这是错误的。这曾经捕获.NET 1.0 / 1.1中所有未处理的异常,但这是一个bug,不应该这样,并且已在.NET 2.0中修复。

AppDomain.CurrentDomain.UnhandledException

仅旨在用作最后一次机会记录沙龙,以便我们可以在程序退出之前记录异常。从2.0版本开始,它不会捕获该异常(尽管在.NET 2.0中至少有一个配置值可以修改以使其表现得像1.1,但建议我们不要使用它。)

值得注意的是,我们几乎无法捕获到一些异常,例如StackOverflowException和OutOfMemoryException。否则,正如其他人所建议的那样,这可能是某个地方的后台线程中的异常。另外,我很确定我们也不会捕获某些/所有非托管/本地异常。

回答

我们使用的是.Net 1.0还是1.1?如果是这样,那么catch(Exception ex)将不会从非托管代码中捕获异常。我们需要改用catch {}。请参阅本文以获取更多详细信息:

http://www.netfxharmonics.com/2005/10/net-20-trycatch-and-trycatchexception/

回答

我不明白...catch块只会引发一个新异常(带有相同消息)。这意味着我们对以下内容的陈述:

The problem is that in some cases (not all) that my try/catch block won't catch it and instead stops execution as an unhandled exception.

正是预期发生的事情。

回答

我同意Daniel Auger和kronoz的观点,这闻起来像是与线程有关的异常。除此之外,还有其他问题:

  • 完整的错误消息说什么?这是什么例外?
  • 根据我们在此处提供的堆栈跟踪,TimeDefLexer.mTokens()中的代码不会引发异常吗?

回答

最好的选择听起来像是将Visual Studio设置为在所有未处理的异常上都中断("调试->异常"对话框,选中"公共语言运行时异常"框,也可能选中其他框)。然后以调试模式运行程序。当ANTLR解析器代码引发异常时,Visual Studio应该捕获该异常,并允许我们查看它的发生位置,异常类型等。

根据描述,catch块似乎是正确的,因此可能发生以下几种情况之一:

  • 解析器实际上并没有引发异常
  • 解析器最终抛出的不是System.Exception派生的东西
  • 在另一个未处理的线程上抛出异常

听起来我们似乎已经排除了问题3.

回答

I traced through the external assembly with Reflector and found no evidence of threading whatsoever.

我们找不到任何线程并不意味着没有线程

.NET有一个"线程池",它是一组"空闲"线程,大部分空闲。某些方法使事情在线程池线程之一中运行,因此它们不会阻止主应用程序。

明显的示例是诸如ThreadPool.QueueUserWorkItem之类的东西,但是还有很多其他事情也可以在线程池中运行看起来不太明显的事情,例如Delegate.BeginInvoke

确实,我们需要按照kibbee的建议进行操作。

回答

To me, a catch (Exception) clause should've captured any exception whatsoever. Is there any reason why it wouldn't?

我能想到的唯一可能性是,其他东西正在我们面前捕获它并以一种似乎未捕获的异常的方式对其进行处理(例如,退出该过程)。

my try/catch block won't catch it and instead stops execution as an unhandled exception.

我们需要查找导致退出过程的原因。这可能是未处理的异常以外的东西。
我们可以尝试将本机调试器与在" {, kernel32.dll} ExitProcess"上设置的断点一起使用。然后使用SOS确定哪些托管代码正在调用退出过程。

回答

我不确定是否不清楚,但是如果这样,我将看到调试器以NoViableAltException类型的"未处理的异常"暂停执行。最初,我对Debug-> Exceptions菜单项一无所知,因为MS希望我们在VS安装时不了解它们之间的区别时提交到配置文件。显然,我不在Cdev配置文件上,并且缺少此选项。最终调试了所有引发的CLR异常之后,很遗憾,我无法发现任何导致此未处理异常问题原因的新行为。引发的所有异常均应在try / catch块中进行处理。

我检查了外部程序集,没有证据表明存在多线程。那样的话,我的意思是不存在对System.Threading的引用,也没有使用任何委托。我熟悉的是实例化线程。我通过在未处理的异常时观察"线程"工具箱来验证这一点,以查看只有一个正在运行的线程。

我与ANTLR员工有一个未解决的问题,因此也许他们以前已经能够解决此问题。我已经能够在VS 2008和VS 2005下使用.NET 2.0和3.5在一个简单的控制台应用程序项目中复制它。

这只是一个痛点,因为它迫使我的代码只能与已知的有效解析器输入一起使用。如果使用IsValid()方法会基于用户输入抛出未处理的异常,则可能会带来风险。当更多地了解此问题时,我将使该问题保持最新状态。

回答

@spoulson,

如果我们可以复制它,可以将其张贴在某个地方吗?我们可以尝试的一种方法是使用带有SOS扩展名的WinDBG运行该应用程序并捕获未处理的异常。它会在第一次机会异常时中断(在运行时尝试找到处理程序之前),此时我们可以看到它来自何处以及什么线程。

如果我们以前从未使用过WinDBG,可能会有些不知所措,但这是一个不错的教程:

http://blogs.msdn.com/johan/archive/2007/11/13/getting-started-with-windbg-part-i.aspx

一旦启动WinDBG,就可以通过转到Debug-> Event Filters来切换未处理异常的中断。

回答

我们是否尝试过在catch子句中打印(Console.WriteLine())异常,而不使用Visual Studio并在控制台上运行应用程序?

回答

我个人完全不相信线程理论。

以前我曾经看过这种情况,当时我正在使用一个库,该库还定义了Exception,而我的用法是指实际的Catch所指的是另一种" Exception"类型(如果已经完全合格,那就是Company。 Lib.Exception,但是由于使用而没有),因此当它要捕获被抛出的普通异常(如果我没有记错的话,就是某种参数异常)时,就不会捕获它,因为类型不匹配。

因此,总而言之,在该类中使用的不同名称空间中是否存在另一个Exception类型?

编辑:一种快速的检查方法是确保在catch子句中将异常类型完全限定为" System.Exception",并对其进行旋转!

EDIT2:好的,我已经尝试过代码并且暂时承认失败。如果没有人提出解决方案,我将不得不在早上进行另一番研究。

回答

我与@Shaun Austin在一起,尝试用完全限定的名称包装尝试

catch (System.Exception)

并查看是否有帮助。ANTLR文档是否说应抛出哪些异常?

回答

嗯,我不明白问题所在。我下载并尝试了示例解决方案文件。

在TimeDefLexer.cs的第852行中引发了异常,该异常随后由Program.cs中的catch块处理,该捕获块仅表示已处理的异常。

如果取消注释其上方的catch块,它将进入该块。

这里似乎是什么问题?

正如Kibbee所说,Visual Studio将在异常上停止,但是如果我们要求它继续执行,则异常将被代码捕获。

回答

我下载了示例VS2008项目,在这里也有些困惑。尽管有可能无法以某种方式工作对我们来说很不错,但我还是能够克服这些例外。但是,这是我发现的:

该邮件列表帖子讨论了我们遇到的相同问题。

从那里,我在main program.cs文件中添加了几个虚拟类:

class MyNoViableAltException : Exception
{
    public MyNoViableAltException()
    {
    }
    public MyNoViableAltException(string grammarDecisionDescription, int decisionNumber, int stateNumber, Antlr.Runtime.IIntStream input)
    {
    }
}
class MyEarlyExitException : Exception
{
    public MyEarlyExitException()
    {
    }

    public MyEarlyExitException(int decisionNumber, Antlr.Runtime.IIntStream input)
    {
    }
}

然后将using行添加到TimeDefParser.cs和TimeDefLexer.cs中:

using NoViableAltException = MyNoViableAltException;
using EarlyExitException = NoViableAltException;

这样,异常将冒泡进入伪造的异常类中并可以在那里进行处理,但是TimeDefLexer.cs的mTokens方法中仍然抛出了异常。在该类的try catch中包装该异常:

try
            {
                alt4 = dfa4.Predict(input);
            }
            catch
            {
            }

我真的不明白为什么不将其包装在内部方法中,而不是在没有进行线程处理的情况下从句柄中调用它,但是无论如何希望将正确的方向指向比我聪明的人。

回答

哇,到目前为止,有2个报告工作正常,有1个遇到了我报告的问题。 Windows,Visual Studio和带有内部版本号的.NET框架是什么版本?

我正在运行XP SP2,VS 2008 Team Suite(9.0.30729.1 SP),C2008(91899-270-92311015-60837)和.NET 3.5 SP1.

回答

我相信我了解这个问题。正在捕获一个例外,问题在于调试器的行为以及每个尝试对其进行复制的人员之间调试器设置的差异引起的混乱。

在Repro的第3种情况下,我相信我们会收到以下消息:" NoViableAltException未由用户代码处理"和如下调用堆栈:

[External Code]    
    >   TestAntlr-3.1.exe!TimeDefLexer.mTokens() Line 852 + 0xe bytes   C#
        [External Code] 
        TestAntlr-3.1.exe!TimeDefParser.prog() Line 141 + 0x14 bytes    C#
        TestAntlr-3.1.exe!TestAntlr_3._1.Program.ParseTest(string Text = "foobar;") Line 49 + 0x9 bytes C#
        TestAntlr-3.1.exe!TestAntlr_3._1.Program.Main(string[] args = {string[0x00000000]}) Line 30 + 0xb bytes C#
        [External Code]

如果在调用堆栈窗口中右键单击并运行,请打开show external code,我们将看到以下内容:

Antlr3.Runtime.dll!Antlr.Runtime.DFA.NoViableAlt(int s = 0x00000000, Antlr.Runtime.IIntStream input = {Antlr.Runtime.ANTLRStringStream}) + 0x80 bytes   
        Antlr3.Runtime.dll!Antlr.Runtime.DFA.Predict(Antlr.Runtime.IIntStream input = {Antlr.Runtime.ANTLRStringStream}) + 0x21e bytes  
    >   TestAntlr-3.1.exe!TimeDefLexer.mTokens() Line 852 + 0xe bytes   C#
        Antlr3.Runtime.dll!Antlr.Runtime.Lexer.NextToken() + 0xc4 bytes 
        Antlr3.Runtime.dll!Antlr.Runtime.CommonTokenStream.FillBuffer() + 0x147 bytes   
        Antlr3.Runtime.dll!Antlr.Runtime.CommonTokenStream.LT(int k = 0x00000001) + 0x2d bytes  
        TestAntlr-3.1.exe!TimeDefParser.prog() Line 141 + 0x14 bytes    C#
        TestAntlr-3.1.exe!TestAntlr_3._1.Program.ParseTest(string Text = "foobar;") Line 49 + 0x9 bytes C#
        TestAntlr-3.1.exe!TestAntlr_3._1.Program.Main(string[] args = {string[0x00000000]}) Line 30 + 0xb bytes C#
        [Native to Managed Transition]  
        [Managed to Native Transition]  
        mscorlib.dll!System.AppDomain.ExecuteAssembly(string assemblyFile, System.Security.Policy.Evidence assemblySecurity, string[] args) + 0x39 bytes    
        Microsoft.VisualStudio.HostingProcess.Utilities.dll!Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly() + 0x2b bytes  
        mscorlib.dll!System.Threading.ThreadHelper.ThreadStart_Context(object state) + 0x3b bytes   
        mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state) + 0x81 bytes    
        mscorlib.dll!System.Threading.ThreadHelper.ThreadStart() + 0x40 bytes

调试器的消息告诉我们,代码外部(来自NoViableAlt)的异常正在遍历我们在TestAntlr-3.1.exe!TimeDefLexer.mTokens()中拥有的代码,而没有得到处理。

措辞令人困惑,但这并不意味着未捕获到例外。调试器让我们知道,我们拥有的mTokens()"代码必须健壮,以防它抛出异常。

对于那些没有重现问题的人,可以看看这是怎么回事:

  • 转到"工具/选项/调试",然后关闭"仅启用我的代码(仅受管理)"。或者选项。
  • 转到调试器/异常,然后为"通用语言运行时异常"关闭"用户未处理"。

回答

我下载了代码,一切正常。

Visual Studio调试器正确拦截所有异常。捕获块按预期方式工作。

我正在运行Windows 2003 Server SP2,VS2008 Team Suite(9.0.30729.1 SP)

我试图为.NET 2.0、3.0和3.5编译项目

@Steve Steiner,我们提到的调试器选项与此行为无关。

我尝试使用这些选项,但没有可见效果的捕获块设法拦截所有异常。

回答

我相信史蒂夫·史坦纳是正确的。在研究Steve的建议时,我碰到了这个话题,谈论的是Tools | Options | Debugger | General中的" Enable Just My Code"选项。建议当非用户代码抛出或者处理异常时,调试器将在某些情况下中断。我不确定为什么这甚至很重要,或者为什么调试器专门说异常在真正发生时没有得到处理,所以我不确定。

通过禁用"启用我的代码"选项,我消除了错误的休息。这也通过删除不再适用的"用户处理"列来更改"调试|异常"对话框。或者,我们只需取消选中CLR的"用户处理"框即可获得相同的结果。

Bigtime谢谢大家的帮助!

回答

Steve Steiner是正确的,该异常起源于antlr库,该异常通过mTokens()方法传递并被捕获在antlr库中。问题在于该方法是由antlr自动生成的。因此,当我们生成解析器/词法分析器类时,为处理mTokens()中的异常所做的任何更改都将被覆盖。

默认情况下,antlr将记录错误并尝试恢复解析。我们可以重写此方法,以便在遇到错误时parser.prog()都将引发异常。从示例代码中,我认为这是我们所期望的行为。

将此代码添加到语法(.g)文件。我们还需要在调试菜单中关闭"启用我的代码"。

@members {

    public override Object RecoverFromMismatchedSet(IIntStream input,RecognitionException e,    BitSet follow)  
    {
        throw e;
    }
}

@rulecatch {
    catch (RecognitionException e) 
    {
        throw e;
    }
}

这是我对"权威ANTLR参考"一书的"在出现第一个错误时退出识别器"一章中给出的示例进行的尝试。

希望这就是我们想要的。