2017-01-30 11 views
5

По this question должно быть гарантировано, что статические поля, которые я использую инициализируются:NullReferenceException от статического одноплодной инициализации встроенного

10.4.5.1 Статическая инициализация поля:

статическое поле инициализаторы переменных одного класса соответствуют a последовательность присвоений, которые выполняются в текстовом порядке в , которые они отображаются в объявлении класса. Если в классе существует статический конструктор (раздел 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. Используйте этот код, чтобы получить TypeInitializationException с NullRefernceException как InnerException в Synchronize на 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 ...... 

Я понимаю, что есть какая-то круговая зависимость здесь (в исходном коде не так очевидно), но я до сих пор не понимаю, почему код не инициализирует синглтоны. Что было бы лучшим подходом для этого случая использования, кроме избежания круговых зависимостей?

+1

Странно, я бы ожидал увидеть 'StackOverflowException'. Как правило, не очень хорошо делать что-то слишком сложное во время конструкторов, особенно когда каждый из них является одиночным, и каждый в конечном итоге вызывает другого, прежде чем либо полностью завершит инициализацию. Возможно, посмотрите на _deferred initialisation_? – MickyD

+3

Если вы посмотрите на это: 'private static SessionManager _instance = new SessionManager()', он выполняет два важных шага. 1.- Инициализация ('new SessionManager()') и 2 asignation ('_instance = obj'). если вы пытаетесь использовать '_instance' перед asignation (как и вы), оно равно null. И Он творит свой NPE. –

ответ

5

Посмотрите на 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(); 
} 
+0

См. Код в обновлении. Если вы используете статический конструктор для разделения экземпляра от инициализации, он работает. –

+0

И о «вызове статического конструктора» статический конструктор состоит из двух шагов. Разумеется, нет видимых вызовов, кроме CLR. –

+0

Это то, что у меня есть сейчас: http://ideone.com/xzceqp –

1

Вы бросаясь шагами и у вас есть какое-то рекурсия происходят:

  1. SessionManager _instance = new SessionManager(); эта строка вызывает некоторые методы, которые заканчиваются вызовом SessionManagerDatabase.GetInstance()

  2. Это также делает то же самое и заканчивается обращением к SessionManager.GetInstance()

  3. Это вызывает проблему, поскольку она требует действительного значения, содержащегося в переменной _instance в SessionManager, но в этот момент вы на самом деле не закончили цепочку вызовов методов для того, чтобы дать правильное значение для _instance тем самым вызывая NullReferenceException.

+0

Спасибо за ответ. Вы правы, я это уже заметил. Но остается вопрос, как избежать этой циклической зависимости. +1 –

+0

@TimSchmelter Возможно, вы захотите переосмыслить свой дизайн. Обычным решением для циклической зависимости является использование интерфейсов. Трудно предложить другой подход, работающий с интерфейсами, поскольку я полагаю, что у вас есть много кода, который вы не показывали. – Deadzone

+0

, конечно, вы правы, есть и другие способы избежать этого. Но на самом деле Патрик и Дэвид уже показали, как легко это исправить. Удалите зависимость от конструктора и вызовите метод, который ссылается на другой класс из статического конструктора. Поскольку используется только один синглтон, нет необходимости вызывать его из конструктора экземпляра. –

1

Что происходит в вашем примере, так это то, что конструктор экземпляра вызывается во время инициализации статического поля согласно спецификации. Но конструктор терпит неудачу с NullReferenceExeption, потому что он пытается получить ссылку на _instance вызовом GetInstance(). Обратите внимание, что _instance еще не инициализирован - процесс инициализации находится в процессе. Следовательно, конструктор экземпляра выходит из строя из-за проблемы выше и поэтому не создает/инициализирует поле _instance. Поэтому коротко вы должны попытаться получить статический _instance из вашего конструктора экземпляра.

+0

Спасибо за ответ. Вы правы, я это уже заметил. Но остается вопрос, как избежать этой циклической зависимости. Я не понимаю, «пытаюсь получить статический _instance из вашего конструктора экземпляра» _ +1 –

+0

Извините, я хотел сказать, что вы не должны пытаться. Что касается решения, я бы предложил использовать Lazy вместо синглтона, основываясь на статическом поле. Lazy гарантирует поточное безопасное одноэлементное выполнение функции инициализации, где вы можете получить всю информацию о сеансах из db. –

3

Если вы посмотрите на это: private static SessionManager _instance = new SessionManager(), он имеет два важных шага.

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

Если вы попытаетесь использовать _instance перед присвоением (как и вы), оно равно null. И это твоя 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 
    } 
+2

Спасибо. Ты прав. Но это именно то, что уже ответил [Patrick] (http://stackoverflow.com/a/41937705/284240) :) –

+2

@TimSchmelter Я сказал вам в своем комментарии, я попытался выучить достаточно C#, чтобы показать решение. Это хорошо известная проблема для меня в java. –