【问题标题】:NullReferenceException from static singleton inline initialization来自静态单例内联初始化的 NullReferenceException
【发布时间】:2017-06-15 16:32:53
【问题描述】:

根据this question,应该保证我使用的静态字段被初始化:

10.4.5.1静态字段初始化:

类的静态字段变量初始化器对应于 以文本顺序执行的任务序列 它们出现在类声明中。如果是静态构造函数 (10.11节)存在于类中,执行静态字段 初始化器在执行该静态之前立即发生 构造函数。否则,静态字段初始化器在 在第一次使用静态之前的依赖于实现的时间 该类的字段

我遇到了一个奇怪的情况,这似乎不是真的。我有两个类,它们相互之间存在循环依赖关系,并且抛出了 NullReferenceException

我能够在以下简化示例中重现此问题,看看:

public class SessionManager
{
    //// static constructor doesn't matter
    //static SessionManager()
    //{
    //    _instance = new SessionManager();
    //}

    private static SessionManager _instance = new SessionManager();
    public static SessionManager GetInstance()
    {
        return _instance;
    }

    public SessionManager()
    {
        Console.WriteLine($"{nameof(SessionManager)} constructor called");
        this.RecoverState();
    }

    public bool RecoverState()
    {
        Console.WriteLine($"{nameof(RecoverState)} called");
        List<SessionInfo> activeSessionsInDb = SessionManagerDatabase.GetInstance().LoadActiveSessionsFromDb();
        // ...
        return true;
    }

    public List<SessionInfo> GetAllActiveSessions()
    {
        Console.WriteLine($"{nameof(GetAllActiveSessions)} called");
        return new List<SessionInfo>();
    }
}

public class SessionManagerDatabase
{
    //// static constructor doesn't matter
    //static SessionManagerDatabase()
    //{
    //    _instance = new SessionManagerDatabase();
    //}

    private static readonly SessionManagerDatabase _instance = new SessionManagerDatabase();
    public static SessionManagerDatabase GetInstance()
    {
        return _instance;
    }

    public SessionManagerDatabase()
    {
        Console.WriteLine($"{nameof(SessionManagerDatabase)} constructor called");
        Synchronize();
    }          

    public void Synchronize()
    {
        Console.WriteLine($"{nameof(Synchronize)} called");
        // NullReferenceException here
        List<SessionInfo> memorySessions = SessionManager.GetInstance().GetAllActiveSessions();  
        //...
    }

    public List<SessionInfo> LoadActiveSessionsFromDb()
    {
        Console.WriteLine($"{nameof(LoadActiveSessionsFromDb)} called");
        return new List<SessionInfo>();
    }
}

public class SessionInfo
{
}

如果您按照其他question 中的建议取消注释静态构造函数,问题仍然存在。使用此代码获取TypeInitializationExceptionNullRefernceException 作为InnerExceptionSynchronize SessionManager.GetInstance().GetAllActiveSessions()

static void Main(string[] args)
{
    try
    {
        var sessionManagerInstance = SessionManager.GetInstance();
    }
    catch (TypeInitializationException e)
    {
        Console.WriteLine(e);
        throw;
    }
}

控制台输出:

SessionManager constructor called
RecoverState called
SessionManagerDatabase constructor called
Synchronize called
System.TypeInitializationException: Der Typeninitialisierer für "SessionManager" hat eine Ausnahme verursacht. ---> System.TypeInitializationException: Der Typeninitialisierer für "SessionManagerDatabase" hat eine Ausnahme verursacht. ---> System.NullReferenceException: Der Objektverweis wurde nicht auf eine Objektinstanz festgelegt.
   bei ConsoleApplication_CSharp.Program.SessionManagerDatabase.Synchronize() in ......
   bei ConsoleApplication_CSharp.Program.SessionManagerDatabase..ctor() in ......
   bei ConsoleApplication_CSharp.Program.SessionManagerDatabase..cctor() in ......
   --- Ende der internen Ausnahmestapelüberwachung ---
   bei ConsoleApplication_CSharp.Program.SessionManagerDatabase.GetInstance()
   bei ConsoleApplication_CSharp.Program.SessionManager.RecoverState() in ......
   bei ConsoleApplication_CSharp.Program.SessionManager..ctor() in .....
   bei ConsoleApplication_CSharp.Program.SessionManager..cctor() in ......
   --- Ende der internen Ausnahmestapelüberwachung ---
   bei ConsoleApplication_CSharp.Program.SessionManager.GetInstance()
   bei ConsoleApplication_CSharp.Program.Main(String[] args) in ......

我知道这里存在某种循环依赖(在原始代码中并不那么明显),但我仍然不明白为什么代码无法初始化单例。除了避免循环依赖之外,这个用例的最佳方法是什么?

【问题讨论】:

  • 很奇怪,我本来希望看到StackOverflowException。在构造函数期间做任何过于复杂的事情通常不是一个好主意,特别是当每个都是单例并且每个最终都在其中一个完全完成初始化之前调用另一个时。也许看看延迟初始化
  • 如果你看这个:private static SessionManager _instance = new SessionManager(),它有两个重要步骤。 1.- 初始化 (new SessionManager()) 和 2 分配 (_instance = the obj)。如果您尝试在分配之前使用_instance(就像您所做的那样),则它为空。它会破坏你的 NPE。

标签: c# static initialization singleton


【解决方案1】:

看看IL:

IL_0001:  newobj     instance void SO.Program/SessionManager::.ctor()
IL_0006:  stsfld     class SO.Program/SessionManager SO.Program/SessionManager::_instance

在这里您可以看到对静态构造函数的调用有两个步骤。它首先初始化一个新实例,然后分配它。这意味着当您进行依赖于实例存在的跨类调用时,您会被卡住。它仍在创建实例的过程中。之后就可以调用了。

您可以通过创建一个静态的Initialize 方法来摆脱这种情况,该方法执行实例化调用。

试试这个:

static SessionManager()
{
    _instance = new SessionManager();

    _instance.RecoverState();
}

static SessionManagerDatabase()
{
    _instance = new SessionManagerDatabase();

    _instance.Synchronize();
}

【讨论】:

  • 查看更新中的代码。如果您使用静态构造函数将实例化与初始化分开,它可以工作。
  • 关于“调用静态构造函数”,静态构造函数由两步组成。当然,除了来自 CLR 的调用之外,没有可见的调用。
  • 谢谢。这确实是在初始化期间解决这种循环依赖的一种工作方式。
【解决方案2】:

您正在匆忙执行步骤,并且正在进行某种递归:

  1. SessionManager _instance = new SessionManager(); 这一行调用了一些以SessionManagerDatabase.GetInstance() 结束的方法

  2. 这也是一样的,并以回调SessionManager.GetInstance()结束

  3. 这会导致问题,因为它需要在 SessionManager 中的 _instance 变量中保存一个有效值,但此时您还没有真正完成方法调用链以便为_instance 从而导致NullReferenceException

【讨论】:

  • 感谢您的回答。你说得对,我已经注意到了。但问题仍然是如何避免这种循环依赖。 +1
  • @TimSchmelter 您可能需要重新考虑您的设计。循环依赖的通常修复方法是使用接口。很难提出使用接口的不同方法,因为我想您有很多未显示的代码。
  • 当然你是对的,还有其他方法可以避免这种情况。但实际上帕特里克和大卫已经展示了如何轻松解决这个问题。从构造函数中移除依赖,并调用从静态构造函数中引用另一个类的方法。由于只使用了单例,因此无需从实例构造函数中调用它。
  • 听起来是个好主意,很高兴你得到了一些快速准确的回复@TimSchmelter :)
【解决方案3】:

在您的示例中发生的是实例构造函数在静态字段初始化期间根据规范被调用。但是构造函数因 NullReferenceExeption 而失败,因为它试图通过 GetInstance() 调用获取对 _instance 的引用。请注意,_instance 尚未初始化 - 初始化过程正在进行中。因此,实例构造函数由于上述问题而失败,因此它不会构造/初始化 _instance 字段。所以简而言之,您应该尝试从您的实例构造函数中获取静态 _instance。

【讨论】:

  • 感谢您的回答。你说得对,我已经注意到了。但问题仍然是如何避免这种循环依赖。我不明白“尝试从您的实例构造函数中获取静态_instance” +1
  • 对不起,我想说你不应该尝试。至于解决方案,我建议使用 Lazy 而不是基于静态字段的单例。 Lazy 保证初始化函数的线程安全单例执行,您可以从数据库中获取所有会话信息。
【解决方案4】:

如果你看这个:private static SessionManager _instance = new SessionManager(),它有两个重要的步骤。

1.- Initialization (new SessionManager()). 
2.- The asignation(_instance = the obj). 

如果您尝试在分配之前使用 _instance(如您所做的那样),则它为空。它会影响您的 NRE。您可以像这样拆分构造函数行为来打破这个回调:

public class SessionManager
{
    private static SessionManager _instance;

    static SessionManager() { 
         _instance = new SessionManager();
         _instance.RecoverState();
    }

    public static SessionManager GetInstance()
    {
        return _instance;
    }

    public SessionManager()
    {
        Console.WriteLine($"{nameof(SessionManager)} constructor called");
        // remove RecoverState() call
    }

【讨论】:

  • 谢谢。你说得对。但这正是Patrick 已经回答的内容:)
  • @TimSchmelter 我在评论中告诉过你,我已经尝试学习足够的 c# 来展示解决方案。这在 java 中对我来说是一个众所周知的问题。
猜你喜欢
  • 2020-05-18
  • 2014-10-06
  • 2022-11-13
  • 1970-01-01
  • 2019-05-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多