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

Я прошел через соответствующий раздел C# Language Spec (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 AU", он вызывается снова (прерывается, если хотите), когда встречается ссылка на BY, вызывая выполнение статических инициализаторов B. Как только статический конструктор B завершится, мы снова вернемся к вызову AU для F(), в котором для BY задано значение 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# (v5.0) указывается, что статический конструктор A (и его статические инициализаторы) запускаются при выполнении "ссылки на любой из статических членов класса". Тем не менее, здесь у нас есть AX, на который ссылаются из F(), и статический конструктор A не запускается (поскольку его статические инициализаторы не работают).

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

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

Спасибо

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 уже началась.

Другие вопросы по тегам