【问题标题】:Better TypeInitializationException (innerException is also null)更好的 TypeInitializationException(innerException 也是 null)
【发布时间】:2016-05-01 20:56:45
【问题描述】:

简介

当用户在 NLog 的配置中创建错误(如无效的 XML),我们(NLog)抛出一个NLogConfigurationException。异常包含错误说明。

但有时如果第一次调用 NLog 来自静态字段/属性,则 NLogConfigurationException 会被 System.TypeInitializationException“吃掉”。

示例

例如如果用户有这个程序:

using System;
using System.Collections.Generic;
using System.Linq;
using NLog;

namespace TypeInitializationExceptionTest
{
    class Program
    {
        //this throws a NLogConfigurationException because of bad config. (like invalid XML)
        private static Logger logger = LogManager.GetCurrentClassLogger();

        static void Main()
        {
            Console.WriteLine("Press any key");
            Console.ReadLine();
        }
    }
}

并且配置有错误,NLog 抛出:

throw new NLogConfigurationException("Exception occurred when loading configuration from " + fileName, exception);

但用户会看到:

“将异常详细信息复制到剪贴板”:

System.TypeInitializationException 未处理 消息:在 mscorlib.dll 中发生“System.TypeInitializationException”类型的未处理异常 附加信息:“TypeInitializationExceptionTest.Program”的类型初始化程序引发了异常。

所以消息消失了!

问题

  1. 为什么 innerException 不可见? (在 Visual Studio 2013 中测试)。
  2. 我可以向TypeInitializationException 发送更多信息吗?喜欢留言吗?我们已经发送了一个 innerException。
  3. 我们是否可以使用另一个异常,或者Exception 上是否有属性以便报告更多信息?
  4. 是否有其他方式可以向用户提供(更多)反馈?

备注

编辑

请注意,我是库维护者,而不是库的用户。 我无法更改调用代码!

【问题讨论】:

  • 这几乎是 CLR 的行为,您无能为力。大多数人应该知道他们需要查看InnerExceptionToString() 还将打印内部异常的消息和堆栈跟踪。你唯一能做的就是返回一个无操作记录器,这样用户的应用程序就不会崩溃,但你会得到明显的影响。
  • @LucasTrzesniewski 但为什么甚至是InnerException null?此外,“将异常详细信息复制到剪贴板”也不会揭示原因! (在 DEBUG 中测试)
  • 对不起,我无法复制这个。我得到一个NLogConfigurationException 类型的非空InnerException 和消息:Error when setting property 'Layout' on NLog.Targets.DatabaseParameterInfo,它本身有一个ArgumentException 类型的InnerException 和消息LayoutRenderer cannot be found: 'thisIsWrong'
  • 这是帖子.. 虽然与答案cshandler.com/2016/02/why-typeinitializationexception-has.html 差别不大
  • 但是,就像@HansPassant 所说,如果您打开“使用托管兼容模式”,则“查看详细信息...”链接在“异常”弹出窗口中可见,您可以在那里看到内部异常.所以内部异常只是没有正确报告。对我来说仍然像一个错误。

标签: c# .net visual-studio exception nlog


【解决方案1】:

我现在看到的唯一解决方案是:

将静态初始化(字段)移动到带有try catch的静态构造函数

【讨论】:

    【解决方案2】:

    System.TypeInitializationException 总是或几乎总是由于不正确初始化类的静态成员而发生。

    您必须通过调试器检查LogManager.GetCurrentClassLogger()。我确定错误发生在该部分代码中。

    //go into LogManager.GetCurrentClassLogger() method
    private static Logger logger = LogManager.GetCurrentClassLogger();
    

    另外我建议你仔细检查你的 app.config 并确保你没有包含任何错误。

    只要静态构造函数抛出异常,或者当您尝试访问静态构造函数抛出异常的类时,就会抛出System.TypeInitializationException

    .NET 加载该类型时,它必须在您第一次使用该类型之前准备好它的所有静态字段。有时,初始化需要运行代码。当代码失败时,你会得到一个System.TypeInitializationException

    根据docs

    当类初始化器无法初始化一个类型时,会创建一个 TypeInitializationException 并传递一个对由该类型的类初始化器抛出的异常的引用。 TypeInitializationException 的 InnerException 属性包含基础异常。 TypeInitializationException 使用值为 0x80131534 的 HRESULT COR_E_TYPEINITIALIZATION。 有关 TypeInitializationException 实例的初始属性值列表,请参阅 TypeInitializationException 构造函数。

    1) InnerException 不可见,这对于这种类型的异常非常典型。 Visual Studio 的版本无关紧要(确保选中“启用异常助手” 选项 - Tools> Options>Debugging>General

    2) 通常TypeInitializationException 隐藏了可以通过InnerException 查看的真正异常。但是下面的测试示例显示了如何填充内部异常信息:

    public class Test {
        static Test() {
            throw new Exception("InnerExc of TypeInitializationExc");
        }
        static void Main(string[] args) {
        }
    }
    

    但有时这个 NLogConfigurationException 会被一个 System.TypeInitializationException 如果第一次调用 NLog 来自 静态字段/属性。

    没什么奇怪的。有人在某处错过了try catch 块。

    【讨论】:

    • 你只是在解释TypeInitializationException 是什么...... OP 似乎很清楚这一点。
    • 是的,谢谢,但我也知道这一点。我将检查“启用异常助手”,感谢您的提示。
    • “没什么奇怪的。有人在某处错过了 try catch 块”不是真的,因为我们建议创建一个静态字段。这无法通过 try catch 解决
    【解决方案3】:

    我看到的原因是入口点类的类型初始化失败。由于没有初始化任何类型,因此类型加载器在TypeInitializationException 中没有任何关于失败类型的报告。

    但是如果你将 logger 的静态初始化器更改为其他类,然后在 Entry 方法中引用该类。您将在 TypeInitialization 异常上获得 InnerException。

    static class TestClass
    {
        public static Logger logger = LogManager.GetCurrentClassLogger();  
    }
    
    class Program
    {            
        static void Main(string[] args)
        {
            var logger = TestClass.logger;
            Console.WriteLine("Press any key");
            Console.ReadLine();
        }
    }
    

    现在您将收到 InnerException,因为已加载 Entry 类型以报告 TypeInitializationException。

    希望您现在明白保持入口点干净并从 Main() 引导应用程序,而不是入口点类的静态属性。

    更新 1

    您也可以使用Lazy<> 来避免在声明时执行配置初始化。

    class Program
    {
        private static Lazy<Logger> logger = new Lazy<Logger>(() => LogManager.GetCurrentClassLogger());
    
        static void Main(string[] args)
        {
            //this will throw TypeInitialization with InnerException as a NLogConfigurationException because of bad config. (like invalid XML)
            logger.Value.Info("Test");
            Console.WriteLine("Press any key");
            Console.ReadLine();
        }
    }
    

    或者,在 LogManager 中尝试 Lazy&lt;&gt; 进行记录器实例化,以便在实际出现第一个日志语句时进行配置初始化。

    更新 2

    我分析了 NLog 的源代码,似乎它已经实现并且很有意义。根据属性“NLog 不应抛出异常,除非由 LogManager.cs 中的属性LogManager.ThrowExceptions 指定”。

    修复 - 在 LogFactory 类中,私有方法 GetLogger() 具有导致异常发生的初始化语句。如果您通过检查属性ThrowExceptions 引入try catch,那么您可以防止初始化异常。

          if (cacheKey.ConcreteType != null)
                {
                    try
                    {
                        newLogger.Initialize(cacheKey.Name, this.GetConfigurationForLogger(cacheKey.Name, this.Configuration), this);
                    }
                    catch (Exception ex)
                    {
                        if(ThrowExceptions && ex.MustBeRethrown())
                        throw;
                    }
                }
    

    此外,最好将这些异常/错误存储在某处,以便可以追踪 Logger 初始化失败的原因,因为它们由于ThrowException 而被忽略。

    【讨论】:

    • 这是一个不错的方法,但我无法更改调用代码。
    • @Julian 我已经用 NLog 库中的修复更新了答案。
    • 感谢对 NLog 的提议更改,但我们知道这一点。我们暂时无法更改此设置,因为这将是一个重大更改。例外是有意的,尽管文档可以使用改进。
    【解决方案4】:

    问题是在第一次引用类时发生静态初始化。在您的 Program 中,它甚至发生在 Main() 方法之前。所以作为经验法则 - 避免任何可能在静态初始化方法中失败的代码。至于您的特定问题 - 请改用惰性方法:

    private static Lazy<Logger> logger = 
      new Lazy<Logger>(() => LogManager.GetCurrentClassLogger());
    
    static void Main() {
      logger.Value.Log(...);
    }
    

    因此,当您第一次访问记录器时,记录器的初始化将会发生(并且可能会失败)——而不是在一些疯狂的静态上下文中。

    更新

    坚持最佳实践最终是图书馆用户的负担。所以如果是我,我会保持原样。但是,如果您真的必须最终解决它,则几乎没有选择:

    1) 永远不要抛出异常 - 这是日志引擎中的有效方法,以及 log4net 的工作原理 - 即

    static Logger GetCurrentClassLogger() {
        try {
           var logger = ...; // current implementation
        } catch(Exception e) {
            // let the poor guy now something is wrong - provided he is debugging
            Debug.WriteLine(e);
            // null logger - every single method will do nothing 
            return new NullLogger();
        }
    }
    

    2) 围绕Logger 类的实现包装惰性方法(我知道您的Logger 类要复杂得多,为了这个问题,我们假设它只有一个方法Log,它需要@987654330 @ 构造Logger 实例。

    class LoggerProxy : Logger {
      private Lazy<Logger> m_Logger;
      // add all arguments you need to construct the logger instance
      public LoggerProxy(string className) {
        m_Logger = new Lazy<Logger>(() => return new Logger(className)); 
      }
      public void Log(string message) {
       m_Logger.Value.Log(message);
      }
    }
    
    static Logger GetCurrentClassLogger() {
      var className = GetClassName();
      return new LoggerProxy(className);
    }
    

    您将摆脱这个问题(真正的初始化将在调用第一个 log 方法时发生,它是向后兼容的方法);唯一的问题是您添加了另一层(我预计性能不会大幅下降,但一些日志引擎确实在进行微优化)。

    【讨论】:

    • 哇!!没看到你的回答。我刚刚更新了我的方法,包括这种方法。希望你不会介意。 :)
    • 这是一个不错的方法,但我无法更改调用代码
    • @Julian - 这是个问题吗?您只需要将其包装为向后兼容 :)
    • 这是一个不错的方法,但可能你可能想让它成为线程安全的,参见例如csharpindepth.com/Articles/General/Singleton.aspx
    • @odrej 我不写调用代码。请检查 TS
    【解决方案5】:

    我在之前的评论中说过,我无法重现您的问题。然后我突然想到您只是在弹出的异常对话框中寻找它,它不显示显示详细信息链接。

    因此,无论如何,您都可以通过以下方式访问InnerException,因为它肯定在那里,除了 Visual Studio 出于某种原因没有报告它(可能是因为它在入口点类型中为vendettamit 想通了)。

    因此,当您运行 tester 分支时,您会看到以下对话框:

    而且它没有显示查看详情链接。

    将异常详细信息复制到剪贴板也不是特别有用:

    System.TypeInitializationException 未处理
    消息:mscorlib.dll 中发生了“System.TypeInitializationException”类型的未处理异常
    附加信息:“TypeInitializationExceptionTest.Program”的类型初始化程序引发了异常。

    现在,使用 OK 按钮关闭对话框,然后前往 Locals 调试窗口。以下是您将看到的内容:

    看到了吗? InnerException 肯定那里,你发现了一个 VS 怪癖:-)

    【讨论】:

    • 不,它不存在,但这在另一篇文章中有解释。没有 innerException 但尚未调用 main
    • @Julian 我写这篇文章是因为我不同意另一个 - 它 存在,正如这个答案所证明的那样,但由于怪癖,VS 没有显示它.我用你的 GitHub 解决方案复制了这个。您是否尝试完全按照我的回答中的步骤进行操作?
    • 我会重新测试它。如果这是真的,那就太好了。
    • 在 VS2013(更新 5)中对其进行了测试,并且 locals 为空。所以行不通:(
    【解决方案6】:

    我只是指出您在这里处理的根本问题。您正在与调试器中的一个错误作斗争,它有一个非常简单的解决方法。使用工具 > 选项 > 调试 > 常规 > 勾选“使用托管兼容模式”复选框。同时取消勾选 Just My Code 以获得最丰富的调试报告:

    如果勾选了“仅我的代码”,则异常报告的信息量较少,但仍可以通过单击“查看详细信息”链接轻松深入了解。

    选项名称是不必要的神秘。它真正所做的是告诉 Visual Studio 使用旧版本的调试引擎。任何使用 VS2013 或 VS2015 的人都会遇到新引擎的问题,可能是 VS2012。这也是之前 NLog 没有解决这个问题的根本原因。

    虽然这是一个很好的解决方法,但发现起来并不容易。程序员也不会特别喜欢使用旧引擎,旧引擎不支持返回值调试和针对 64 位代码的 E+C 等闪亮的新功能。很难猜测这究竟是新引擎中的错误、疏忽还是技术限制。这太难看了,所以不要犹豫,给它贴上“bug”的标签,我强烈建议你把它带到 connect.microsoft.com。当它得到修复时,每个人都会领先,我记得至少有一次为此挠头。当时使用 Debug > Windows > Exceptions > 勾选 CLR Exceptions 来深入了解它。

    这种非常不幸的行为的解决方法肯定是丑陋的。您必须延迟引发异常,直到程序执行进展到足够远。我不太了解您的代码库,但是延迟解析配置直到第一个日志记录命令应该处理它。或者存储异常对象并在第一个日志命令中抛出它,可能更容易。

    【讨论】:

    • 可以确认这将在“使用托管兼容模式”打开时打印 innerException(VS2015 更新 1)。谢谢!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-04-16
    • 2011-05-21
    • 2011-01-24
    • 2012-09-10
    • 2012-11-12
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多