Статическое свойство равно null после назначения

У меня есть этот код:

static class Global
{
    public static readonly IChannelsData Channels = new ChannelsData();
    public static readonly IMessagesData Messages = new MessagesData();
}

Мое понимание состоит в том, что, поскольку этот класс является статическим, невозможно, чтобы Global.Channels или Global.Messages был null, когда им был предоставлен экземпляр.

Однако я пытаюсь получить доступ к свойству с помощью

public class Channel : IComparable
{

    ...

    private SortedList<string, Message> _messages;

    [JsonConstructor]
    public Channel()
    {
        _messages = new SortedList<string, Message>();
    }

    [OnDeserialized]
    private void Init(StreamingContext context)
    {
        **Global.Channels.RegisterChannel(this);**
    }  

    ...

}

Я получаю NullReferenceException на Global.Channels, который я подтвердил в ближайшем окне. Дальше меня сбивает с толку, я могу попасть в точку останова на new ChannelData(), поэтому я знаю, что статический член заполняется - успешно - в какой-то момент.

Больше контекста, запрос комментариев:

    private Hashtable _channels;

    public ChannelsData()
    {
        _channels = new Hashtable();

        foreach(Channel channel in SlackApi.ChannelList())
        {
            _channels.Add(channel.GetHashCode(), channel);
        }
    }

Это похоже на что-то похожее на проблему здесь. Однако в моей ситуации я десериализую использование JSON.NET, а не WCF, и рассматриваемое свойство находится в отдельном статическом классе, а не в том же классе. Я также не могу использовать обходное решение для решения, размещенного там.

Полная трассировка стека:

в Vert.Slack.Channel.Init(контекст StreamingContext) в C:\\Vert\Slack\Channel.cs: строка 48

И ошибка:

Ссылка на объект не установлена ​​в экземпляр объекта.

Ответы

Ответ 1

Я смог воспроизвести его со следующим:

class Program
{
    static void Main(string[] args)
    {
        var m = Global.Messages;
    }
}
[Serializable]
public class Blah
{
    [OnDeserialized]
    public void DoSomething(StreamingContext context)
    {
        Global.Channels.DoIt(this);
    }
}
static class Global
{
    private static Blah _b = Deserialize();

    public static readonly IChannelsData Channels = new ChannelsData();
    public static readonly IMessagesData Messages = new MessagesData();

    public static Blah Deserialize()
    {
        var b = new Blah();
        b.DoSomething(default(StreamingContext));
        return b;
    }
}

По существу, порядок выполнения:

var m = Global.Messages; приводит к запуску статического инициализатора для Global.

Согласно ECMA-334 относительно инициализации статического поля:

Инициализаторы статической переменной поля объявления класса соответствуют последовательности назначений, которые выполняются в текстовый порядок, в котором они отображаются в объявлении класса. Если статический конструктор (§17.11) существует в классе, выполнение инициализаторы статического поля возникают непосредственно перед выполнением этого статический конструктор. Иначе инициализаторы статического поля выполненных в зависящее от реализации время до первого использования статическое поле этого класса

Это основная причина. См. Комментарии к большему контексту по круговой ссылке

Это означает, что мы вызываем Deserialize и нажимаем Global.Channels.DoIt(this); до того, как инициализатор имеет возможность завершить настройку. Насколько мне известно, это единственный способ, с которого статическое поле не может быть инициализировано до его использования - после некоторого тестирования они действительно созданы даже при использовании диспетчеров времени выполнения (dynamic), отражения и GetUninitializedObject ( для последнего инициализация выполняется при первом вызове метода, однако).

Хотя ваш код может быть менее очевидным для диагностики (например, если цепочка запускается другим статическим классом). Например, это вызовет ту же проблему, но не сразу станет ясно:

class Program
{
   static void Main(string[] args)
   {
       var t = Global.Channels;
   }
}
[Serializable]
public class Blah
{
   [OnDeserialized]
   public void DoSomething(StreamingContext context)
   {
       Global.Channels.DoIt();
   }
}

public interface IChannelsData { void DoIt(); }
class ChannelsData : IChannelsData
{
    public static Blah _b = Deserialize();
    public static Blah Deserialize()
    {
        var b = new Blah();
        b.DoSomething(default(StreamingContext));
        return b;
    }
    public void DoIt() 
    {
        Console.WriteLine("Done it");
    }
}

static class Global
{
    public static readonly IChannelsData Channels = new ChannelsData();
    public static readonly IMessagesData Messages = new MessagesData();  
}

Итак:

  • Если у вас есть что-то еще в Globals перед этими полями, вы должны исследовать их (если они были оставлены для краткости). Это может быть просто, как перенос объявления Channels в начало класса.
  • Осмотреть ChannelsData для любых статических ссылок и следовать за ними в исходное.
  • Установка точки останова в DoSomething должна дать вам трассировку стека обратно к статическим инициализаторам. Если это не так, попробуйте реплицировать проблему, вызвав new Blah(default(StreamingContext)), где она обычно будет десериализована.