Статический инициализатор C# со смешанными статическими конструкторами (и без них)

Я просмотрел соответствующий раздел Спецификации языка С# (v5.0), но не могу найти часть, относящуюся к тому, что я вижу.

Если у вас есть запуск приведенного ниже кода, вы увидите вывод ниже, чего я и ожидаю:

using System;

class Test {
   static int count = 0;
   static void Main() {
      Console.WriteLine("In Main(), A.X=" + A.X);
   }

   public static int F(string message) {
      Console.WriteLine(message);
      A.X = ++count;

      Console.WriteLine("\tA.X has been set to " + A.X);
      B.Y = ++count;

      Console.WriteLine("\tB.Y has been set to " + B.Y);
      return 999;
   }
}
class A {
   static A() { }
   public static int U = Test.F("Init A.U");
   public static int X = Test.F("Init A.X");
}

class B {
   static B() { }
   public static int R = Test.F("Init B.R");
   public static int Y = Test.F("Init B.Y");
}

Результат:

Init A.U
    A.X has been set to 1
Init B.R
    A.X has been set to 3
    B.Y has been set to 4
Init B.Y
    A.X has been set to 5
    B.Y has been set to 6
    B.Y has been set to 2
Init A.X
    A.X has been set to 7
    B.Y has been set to 8
In Main(), A.X=999

Это именно тот результат, которого я ожидал. В частности, обратите внимание, что хотя метод F() выполняется с параметром "Init A.U", он вызывается снова (прерывается, если хотите) после того, как встречается ссылка на B.Y, вызывая выполнение статических инициализаторов B. Как только статический конструктор B завершится, мы снова вернемся к вызову A.U функции F(), который объясняет, что B.Y устанавливается в 6, а затем в 2. Так что, надеюсь, этот результат будет понятным для всех.

Вот что я не понимаю: если вы закомментируете статический конструктор B, вы увидите следующий вывод:

Init B.R
        A.X has been set to 1
        B.Y has been set to 2
Init B.Y
        A.X has been set to 3
        B.Y has been set to 4
Init A.U
        A.X has been set to 5
        B.Y has been set to 6
Init A.X
        A.X has been set to 7
        B.Y has been set to 8
In Main(), A.X=999

В разделах 10.5.5.1 и 10.12 C# Spec (v5.0) указано, что статический конструктор A (и его статические инициализаторы) запускается для выполнения, когда «ссылаются на любой из статических членов класса». Однако здесь мы имеем ссылку на A.X из F(), а статический конструктор A не запускается (поскольку его статические инициализаторы не запущены).

Поскольку A имеет статический конструктор, я ожидаю, что эти инициализаторы будут запускать (и прерывать) вызов «Init BR» для F(), точно так же, как статический конструктор B прервал вызов A для F() в вызове «Init AU», который я показал с начала.

Кто-нибудь может объяснить? На первый взгляд это выглядит как нарушение спецификации, если только это не допускается какой-либо другой частью спецификации.

Спасибо


person Tom Baxter    schedule 17.04.2015    source источник
comment
Как только вы удалите статический конструктор, все ставки будут сняты, когда запустится инициализатор типа. Его можно запустить в любое время до доступа к статическим полям. Это описано в разделе 10.5.5.1. Он запускается до того, как вы получите доступ к полям, поэтому я не вижу нарушений. Почему он запускается до запуска статических инициализаторов A, является интересным вопросом. Я полагаю, что в .NET 4.5 классы beforefieldinit запускают инициализатор своего типа, когда JIT загружает информацию о типе, но если есть статический конструктор, он запускается при фактическом доступе к членам. Здесь JITting Test.F требует загрузки информации о типе B.   -  person Mike Zboray    schedule 18.04.2015
comment
В дополнение к комментариям Майка стоит отметить, что это поведение действительно изменилось с течением времени. В качестве примера взгляните на наблюдения Джона Скита в его статье Введите изменения инициализации в .NET 4.0. Суть в том, что, за исключением некоторых конкретных сценариев, точный порядок выполнения статической инициализации не гарантируется, и ваш код не должен рассчитывать на какой-либо конкретный порядок. К счастью, спецификация гарантирует порядок инициализации, когда вы этого ожидаете, например, когда один тип зависит от другого и т. д.   -  person Peter Duniho    schedule 18.04.2015
comment
@mike z - Имейте в виду, что я удалил статический c'tor из B, а не из A. Среда выполнения, безусловно, может свободно запускать статические инициализаторы B на досуге. Однако, поскольку A имеет статический c'tor (и, следовательно, не имеет beforefieldinit), статический c'tor A должен, согласно спецификации (разделы 10.5.5.1 и 10.12), запускаться ссылкой на AX в F(). , но это не то, что происходит. На A.X ссылаются с помощью F(), но статический c'tor A не выполняется. Может быть, я что-то упускаю, но, судя по тому, что я вижу, это нарушает спецификацию.   -  person Tom Baxter    schedule 18.04.2015
comment
@Peter Duniho - Спецификация дает некоторые гарантии относительно времени статических инициализаторов, если класс содержит статический конструктор: см. разделы 10.5.5.1 и 10.12 спецификации версии 5.0. Позвольте мне просто процитировать сек. 10.12: Выполнение статического конструктора запускается первым из следующих событий, происходящих в домене приложения: • Создается экземпляр типа класса. • Ссылаются на любые статические члены типа класса. Поскольку A.X упоминается в Test.F(), в соответствии со спецификацией должно быть так, что выполняются статические инициализаторы A.   -  person Tom Baxter    schedule 18.04.2015
comment
@TomBaxter: Спецификация дает некоторые гарантии в отношении времени статических инициализаторов, если класс содержит статический конструктор -- да, я хорошо знаю. Отсюда и слова ...за исключением некоторых конкретных сценариев в моем комментарии.   -  person Peter Duniho    schedule 18.04.2015
comment
@TomBaxter Нет, он не запускается ссылкой на AX внутри, потому что F (), потому что он уже находится в процессе запуска инициализатора типа A при доступе к AX в Main. Вы можете увидеть это, если распечатаете трассировку стека в начале F. В качестве альтернативы добавьте еще один уровень косвенности, чтобы метод доступа к полям не совпадал с тем, который печатает сообщение.   -  person Mike Zboray    schedule 18.04.2015
comment
@mike z Привет, Майк, я пытался указать, что, хотя F() выполняется в контексте статических инициализаторов класса B, он может и должен быть прерван, когда он (то есть F()) встречает назначение для AX, так как A имеет статический c'tor. Это прерывание F() — это то, что мы видим, когда и A, и B имеют статические c'tors. Разрешить назначение AX внутри F(), когда существует статический c'tor для A(), является нарушением (кажется) 10.12. Опять же, вероятно, я что-то неправильно интерпретирую.   -  person Tom Baxter    schedule 18.04.2015


Ответы (1)


Мне кажется, я понимаю, что здесь происходит, хотя у меня нет хорошего объяснения, почему это происходит именно так.

Тестовая программа слишком грубая, чтобы увидеть, что происходит. Сделаем небольшую корректировку:

class Test {
   static int count = 0;
   static void Main() {
      Console.WriteLine("In Main(), A.X=" + A.X);
   }

   public static int F(string message) {
       Console.WriteLine("Before " + message);
       return FInternal(message);
   }

   private static int FInternal(string message) {
      Console.WriteLine("Inside " + message);
      A.X = ++count;

      Console.WriteLine("\tA.X has been set to " + A.X);
      B.Y = ++count;

      Console.WriteLine("\tB.Y has been set to " + B.Y);
      return 999;
   }
}
class A {
   static A() { }
   public static int U = Test.F("Init A.U");
   public static int X = Test.F("Init A.X");
}

class B {
   static B() { }
   public static int R = Test.F("Init B.R");
   public static int Y = Test.F("Init B.Y");
}

Вывод аналогичен тому, что указан в вопросе, но более подробно:

Before Init A.U  
Inside Init A.U  
    A.X has been set to 1  
Before Init B.R  
Inside Init B.R  
    A.X has been set to 3  
    B.Y has been set to 4  
Before Init B.Y  
Inside Init B.Y  
    A.X has been set to 5  
    B.Y has been set to 6  
    B.Y has been set to 2  
Before Init A.X  
Inside Init A.X  
    A.X has been set to 7  
    B.Y has been set to 8  
In Main(), A.X=999

Здесь нет ничего удивительного. Удалите статический конструктор B, и вот что вы получите:

Before Init A.U  
Before Init B.R  
Inside Init B.R  
    A.X has been set to 1  
    B.Y has been set to 2  
Before Init B.Y  
Inside Init B.Y  
    A.X has been set to 3  
    B.Y has been set to 4  
Inside Init A.U  
    A.X has been set to 5  
    B.Y has been set to 6  
Before Init A.X  
Inside Init A.X  
    A.X has been set to 7  
    B.Y has been set to 8  
In Main(), A.X=999

Теперь это интересно. Мы видим, что первоначальный вывод вводил в заблуждение. На самом деле мы начинаем с попытки инициализировать A.U. Это неудивительно, потому что A должен быть инициализирован первым, потому что A.X доступен в Main. Следующая часть интересная. Похоже, что когда B не имеет статического конструктора, CLR прерывает метод, который собирается получить доступ к полям B (FInternal) прежде чем он войдет в метод. Сравните это с другим случаем. Там инициализация B была отложена до тех пор, пока мы фактически не получили доступ к полям B.

Я не совсем уверен, почему все делается именно в таком порядке, но вы видите, что причина, по которой инициализация B не прерывается для инициализации A, заключается в том, что инициализация A уже началась.

person Mike Zboray    schedule 18.04.2015