51

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

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

    Expl.Itinerary.dll!TimeDefLexer.mTokens() 第 1213 行 C#
    Antlr3.Runtime.dll!Antlr.Runtime.Lexer.NextToken() + 0xfc 字节
    Antlr3.Runtime.dll!Antlr.Runtime.CommonTokenStream.FillBuffer() + 0x22c 字节   
    Antlr3.Runtime.dll!Antlr.Runtime.CommonTokenStream.LT(int k = 1) + 0x68 字节
    Expl.Itinerary.dll!TimeDefParser.prog() 第 109 行 + 0x17 字节 C#
    Expl.Itinerary.dll!Expl.Itinerary.TDLParser.Parse(string Text = "", Expl.Itinerary.IItinerary Itinerary = {Expl.Itinerary.MemoryItinerary}) 第 17 行 + 0xa 字节 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。我遇到的异常处理问题永远不会到达那行代码。事实上,我注释掉了“throw new 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 没有进行多线程。

4

25 回答 25

30

我相信我理解这个问题。异常被捕获,问题是对调试器行为的混淆以及每个尝试重现它的人之间调试器设置的差异。

在您的 repro 的第 3 种情况下,我相信您会收到以下消息:“NoViableAltException 未被用户代码处理”和一个如下所示的调用堆栈:

         [外部代码]    
    > TestAntlr-3.1.exe!TimeDefLexer.mTokens() 第 852 行 + 0xe 字节 C#
        [外部代码]
        TestAntlr-3.1.exe!TimeDefParser.prog() 第 141 行 + 0x14 字节 C#
        TestAntlr-3.1.exe!TestAntlr_3._1.Program.ParseTest(string Text = "foobar;") 第 49 行 + 0x9 字节 C#
        TestAntlr-3.1.exe!TestAntlr_3._1.Program.Main(string[] args = {string[0x00000000]}) 第 30 行 + 0xb 字节 C#
        [外部代码]

如果您在调用堆栈窗口中右键单击并运行打开显示外部代码,您会看到:

        Antlr3.Runtime.dll!Antlr.Runtime.DFA.NoViableAlt(int s = 0x00000000, Antlr.Runtime.IIntStream 输入 = {Antlr.Runtime.ANTLRStringStream}) + 0x80 字节   
        Antlr3.Runtime.dll!Antlr.Runtime.DFA.Predict(Antlr.Runtime.IIntStream 输入 = {Antlr.Runtime.ANTLRStringStream}) + 0x21e 字节  
    > TestAntlr-3.1.exe!TimeDefLexer.mTokens() 第 852 行 + 0xe 字节 C#
        Antlr3.Runtime.dll!Antlr.Runtime.Lexer.NextToken() + 0xc4 字节
        Antlr3.Runtime.dll!Antlr.Runtime.CommonTokenStream.FillBuffer() + 0x147 字节   
        Antlr3.Runtime.dll!Antlr.Runtime.CommonTokenStream.LT(int k = 0x00000001) + 0x2d 字节  
        TestAntlr-3.1.exe!TimeDefParser.prog() 第 141 行 + 0x14 字节 C#
        TestAntlr-3.1.exe!TestAntlr_3._1.Program.ParseTest(string Text = "foobar;") 第 49 行 + 0x9 字节 C#
        TestAntlr-3.1.exe!TestAntlr_3._1.Program.Main(string[] args = {string[0x00000000]}) 第 30 行 + 0xb 字节 C#
        [本机到托管转换]  
        [管理到本地转换]  
        mscorlib.dll!System.AppDomain.ExecuteAssembly(string assemblyFile, System.Security.Policy.Evidence assemblySecurity, string[] args) + 0x39 字节    
        Microsoft.VisualStudio.HostingProcess.Utilities.dll!Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly() + 0x2b 字节  
        mscorlib.dll!System.Threading.ThreadHelper.ThreadStart_Context(对象状态)+ 0x3b 字节   
        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 字节

调试器的消息告诉您,源自您的代码(来自 NoViableAlt)之外的异常正在通过您在 TestAntlr-3.1.exe!TimeDefLexer.mTokens() 中拥有的代码而没有被处理。

措辞令人困惑,但这并不意味着例外未被发现。调试器让您知道您拥有的代码 mTokens()" 需要对通过它抛出的异常具有鲁棒性。

可以玩的东西,看看这对于那些没有重现问题的人来说是怎样的:

  • 转到工具/选项/调试并关闭“仅启用我的代码(仅限托管)”。或选项。
  • 转到调试器/异常并关闭通用语言运行时异常的“用户未处理”。
于 2008-09-03T05:27:31.937 回答
9

无论程序集是否已编译为发布版本,异常肯定应该“冒泡”到调用者,没有理由没有在调试模式下编译的程序集对此有任何影响。

我同意 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;
 }
于 2008-08-30T15:34:44.723 回答
8

我可以告诉你这里发生了什么...

Visual Studio 正在中断,因为它认为异常未处理。未处理是什么意思?好吧,在 Visual Studio 中,工具...选项...调试...常规...“仅启用我的代码(仅限托管)”中有一个设置。如果选中此项,并且如果异常从您的代码传播到与“不是您的代码”(例如,Antlr)的程序集中存在的方法调用关联的堆栈帧,则被认为是“未处理的”。出于这个原因,我关闭了 Enable Just My Code 功能。但是,如果你问我,这很蹩脚......假设你这样做:

ExternalClassNotMyCode c = new ExternalClassNotMyCode();
try {
    c.doSomething( () => { throw new Exception(); } );
}
catch ( Exception ex ) {}

doSomething 在那里调用您的匿名函数,并且该函数引发异常......

请注意,如果启用“仅启用我的代码”,则根据 Visual Studio,这是一个“未处理的异常”。另外,请注意,它在调试模式下就像断点一样停止,但在非调试或生产环境中,代码完全有效并按预期工作。此外,如果您只是在调试器中“继续”,则应用程序会以愉快的方式继续运行(它不会停止线程)。它被认为是“未处理的”,因为异常通过不在您的代码中(即在外部库中)的堆栈帧传播。如果你问我,这很糟糕。请更改此默认行为 Microsoft。这是使用异常来控制程序逻辑的一个完全有效的案例。有时,您无法将第三方库更改为其他方式,

以 MyBatis 为例,您可以使用此技术来停止处理通过调用 SqlMapper.QueryWithRowDelegate 收集的记录。

于 2012-01-20T19:33:28.680 回答
5

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

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

于 2008-08-30T16:07:42.960 回答
4

我和@Shaun Austin 在一起 - 尝试用完全限定名称包装 try

catch (System.Exception)

看看是否有帮助。ANTLR 文档是否说应该抛出什么异常?

于 2008-09-02T17:08:45.083 回答
3

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

于 2008-08-30T15:10:36.170 回答
2

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

我能想到的唯一可能是其他东西在你之前捕获它并以一种似乎是未捕获的异常的方式处理它(例如退出进程)。

我的 try/catch 块不会捕获它,而是作为未处理的异常停止执行。

您需要找出导致退出过程的原因。它可能不是未处理的异常。您可以尝试使用在“{,,kernel32.dll}ExitProcess”上设置断点的本机调试器。然后使用SOS确定哪些托管代码正在调用退出进程。

于 2008-09-02T04:28:00.370 回答
2

就我个人而言,我根本不相信线程理论。

有一次我以前见过这个,我正在使用一个库,它也定义了异常和使用我的意思是实际的 Catch 指的是不同的“异常”类型(如果它已经完全限定它是 Company. Lib.Exception 但它不是因为使用)所以当它捕获一个被抛出的正常异常(如果我没记错的话,某种参数异常)它只是不会捕获它,因为类型不匹配。

因此,总而言之,在该类的 using 中,在不同的命名空间中是否还有另一个 Exception 类型?

编辑:检查这一点的快速方法是确保在您的 catch 子句中您完全将异常类型限定为“System.Exception”并试一试!

EDIT2:好的,我已经尝试了代码并暂时承认失败。如果没有人提出解决方案,我将不得不在早上再看一遍。

于 2008-09-02T16:00:45.823 回答
2

嗯,我不明白这个问题。我下载并尝试了您的示例解决方案文件。

在 TimeDefLexer.cs 的第 852 行中引发了一个异常,该异常随后由 Program.cs 中的 catch 块处理,该块只是说Handled exception

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

这里似乎有什么问题?

正如 Kibbee 所说,Visual Studio 将停止异常,但如果您要求它继续,异常将被您的代码捕获。

于 2008-09-02T20:47:39.783 回答
2

我下载了示例 VS2008 项目,在这里也有点难过。但是,我能够克服异常,尽管可能不会以一种对您有用的方式。但这是我发现的:

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

从那里,我在主 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
            {
            }

我真的不明白为什么将它包装在内部方法中,而不是从处理错误的地方调用它,如果线程不在播放中,但无论如何希望这将指向比我更聪明的人在正确的方向上。

于 2008-09-02T22:27:54.363 回答
2

我下载了你的代码,一切都按预期工作。

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

我正在运行 Windows 2003 server SP2、VS2008 Team Suite (9.0.30729.1 SP)

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

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

我试图在没有明显效果的情况下使用这些选项 - catch 块设法拦截所有异常。

于 2008-09-03T05:44:32.970 回答
2

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;
    }
}

这是我对“Definitive ANTLR Reference”一书的“Exiting the recogniser on first error”一章中给出的示例的 C# 版本的尝试。

希望这就是你要找的。

于 2009-03-10T12:57:13.393 回答
1

您可以将 VS.Net 设置为在发生任何异常时立即中断。只需在调试模式下运行您的项目,一旦抛出异常,它将立即停止。然后你应该更好地了解为什么它没有被抓住。

此外,您可以放入一些代码来捕获所有未处理的异常。

Application.ThreadException += new ThreadExceptionEventHandler(ThreadExceptionHandler);

 // Catch all unhandled exceptions in all threads.
 AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(UnhandledExceptionHandler);
于 2008-08-30T15:36:05.063 回答
1

哦,关于基比所说的;如果您在 VS 中选择 Debug|Exceptions 并单击“throw”列中的所有框,它应该将 AFAIK 中的所有内容都选为“first chance exception”,即 VS 将指示异常何时将由其他所有内容处理,并且打破相关代码。这应该有助于调试。

于 2008-08-30T15:42:48.997 回答
1

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

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

  1. 解析器实际上并没有抛出异常
  2. 解析器最终会抛出一些不是从 System.Exception 派生的东西
  3. 在另一个未处理的线程上抛出异常

听起来您可能已经排除了问题 #3。

于 2008-08-31T14:58:10.733 回答
1

我用反射器追踪了外部组件,没有发现任何螺纹的迹象。

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

.NET 有一个“线程池”,它是一组“备用”线程,大部分时间处于空闲状态。某些方法会导致事情在线程池线程之一中运行,因此它们不会阻塞您的主应用程序。

明显的例子是像ThreadPool.QueueUserWorkItem这样的东西,但是还有很多其他的东西也可以在线程池中运行看起来不那么明显的东西,比如Delegate.BeginInvoke

真的,你需要按照kibbee 的建议去做

于 2008-09-01T04:02:01.177 回答
1

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

于 2008-09-02T15:45:25.173 回答
1

我相信史蒂夫·施泰纳是正确的。在研究史蒂夫的建议时,我遇到了这个线程,它在谈论工具|选项|调试器|常规中的“启用我的代码”选项。建议调试器在非用户代码抛出或处理异常时在某些情况下中断。我不确定为什么这甚至很重要,或者为什么调试器特别说异常在实际情况下未被处理。

通过禁用“仅启用我的代码”选项,我能够消除错误中断。这也通过删除“用户处理”列来更改调试|异常对话框,因为它不再适用。或者,您可以取消选中 CLR 的“用户处理”框并获得相同的结果。

Bigtime感谢大家的帮助!

于 2008-09-03T10:17:12.927 回答
0

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

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

AppDomain.CurrentDomain.UnhandledException 

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

值得注意的是,您无法捕获的异常很少,例如 StackOverflowException 和 OutOfMemoryException。否则,正如其他人所建议的那样,它可能是某个后台线程中的异常。另外,我很确定您也无法捕获某些/所有非托管/本机异常。

于 2008-08-30T16:04:42.657 回答
0

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

问题是在某些情况下(不是全部)我的 try/catch 块不会捕获它,而是作为未处理的异常停止执行。

正是预期会发生的事情。

于 2008-08-30T16:10:28.133 回答
0

我同意Daniel Augerkronoz的观点,这听起来像是一个与线程有关的异常。除此之外,这是我的其他问题:

  1. 完整的错误信息是什么意思?这是什么异常?
  2. 根据您在此处提供的堆栈跟踪,您在 TimeDefLexer.mTokens() 中的代码不是抛出异常吗?
于 2008-08-30T16:19:55.240 回答
0

我不确定我是否不清楚,但如果是这样,我看到调试器停止执行并出现 NoViableAltException 类型的“未处理异常”。最初,我对这个 Debug->Exceptions 菜单项一无所知,因为 MS 希望您在 VS 安装时,在您不知道它们有何不同时提交配置文件。显然,我不在 C# 开发配置文件中,并且缺少此选项。在最终调试所有抛出的 CLR 异常之后,不幸的是,我无法发现任何导致此未处理异常问题的原因的新行为。所有抛出的异常都是预期的,并且应该在 try/catch 块中处理。

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

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

这只是一个痛点,因为它迫使我的代码只能使用已知的有效解析器输入。IsValid()如果根据用户输入抛出未处理的异常,则使用方法将是有风险的。当更多地了解这个问题时,我会及时更新这个问题。

于 2008-09-02T14:08:43.860 回答
0

@spoulson,

如果你能复制它,你能把它贴在某个地方吗?您可以尝试的一种方法是使用带有 SOS 扩展的 WinDBG 来运行应用程序并捕获未处理的异常。它将在第一次机会异常时中断(在运行时尝试查找处理程序之前),您可以在那时看到它来自哪里,以及哪个线程。

如果您以前没有使用过 WinDBG,可能会有点不知所措,但这里有一个很好的教程:

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

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

于 2008-09-02T14:30:56.630 回答
0

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

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

于 2008-09-03T01:21:06.943 回答
0

如果您在项目中使用 com 对象并尝试捕获块而不捕获异常,则当异常跨越 AppDomain 或托管/本机边界(仅限托管)选项时,您将需要禁用工具/调试/中断。

于 2017-02-17T09:19:39.177 回答