C#静态初始化器与(和没有)混合静态构造函数

7
我已经阅读了相关的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

C#规范(v5.0)的10.5.5.1和10.12节指出,当“类的任何静态成员被引用”时,将触发A的静态构造函数(及其静态初始化器)执行。然而,在这里我们从F()内部引用了A.X,但A的静态构造函数没有被触发(因为它的静态初始化器没有运行)。
由于A有一个静态构造函数,我期望这些初始化器会运行(并中断)对F()的“Init B.R”调用,就像B的静态构造函数在一开始的“Init A.U”调用中中断了对A的F()调用一样。
有人能解释一下吗?表面上看起来似乎违反了规范,除非规范的其他部分允许这样做。
谢谢

2
一旦您删除静态构造函数,当类型初始化程序运行时,所有的赌注都会失效。它可以在访问静态字段之前的任何时间运行。这在第10.5.5.1节中有描述。它在您访问字段之前运行,所以我认为没有违规。为什么它在A的静态初始化器运行之前运行是一个有趣的问题。我相信,在.NET 4.5中,beforefieldinit类在JIT加载类型信息时运行其类型初始化程序,但如果有静态构造函数,则在实际访问成员时运行它。这里JITting Test.F需要加载B的类型信息。 - Mike Zboray
除了Mike的评论外,值得注意的是这种行为实际上随着时间的推移而改变。例如,请看Jon Skeet在他的文章《.NET 4.0中的类型初始化更改》中的观察。最重要的是,除了一些特定的情况外,静态初始化的确切顺序没有保证,您的代码不应该依赖于任何特定的顺序。幸运的是,规范确保了在您期望的情况下(例如,当一个类型依赖于另一个类型时)的初始化顺序。 - Peter Duniho
请注意,我已将B的静态构造函数移除,而非A的静态构造函数。运行时可以自由地运行B的静态初始化器。然而,因为A有一个静态构造函数(因此没有beforefieldinit),按照规范(第10.5.5.1和10.12节),A的静态构造函数必须在F()中引用A.X时触发,但事实并非如此。A.X在F()内被引用,但A的静态构造函数没有执行。也许我漏了什么,但从我的角度看,这似乎违反了规范。 - Tom Baxter
@Peter Duniho - 如果该类包含静态构造函数,则规范确实对静态初始化程序的定时进行了一些保证:请参阅版本5.0规范的10.5.5.1和10.12节。让我引用第10.12节:应用程序域内的以下事件中第一个会触发静态构造函数的执行:• 创建类类型的实例。 • 引用类类型的任何静态成员。由于Test.F()中引用了A.X,因此按照规范,应运行A的静态初始化程序。 - Tom Baxter
@TomBaxter: “如果类包含静态构造函数,则规范确实对静态初始化程序的时间做出了一些保证” - 是的,我非常清楚。因此,在我的评论中加入了“除了某些特定情况”的字眼。 - Peter Duniho
显示剩余2条评论
1个回答

2
我想我知道这里发生了什么,尽管我没有一个好的解释为什么会这样。
测试程序有点粗糙,无法看清发生了什么。让我们进行一些小的调整:
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应该首先初始化,因为Main中访问了A.X。接下来的部分很有趣。当B没有静态构造函数时,CLR会在进入方法之前中断将要访问B字段(FInternal)的方法。与另一种情况形成对比。在那里,B的初始化被延迟,直到我们实际访问B的字段。我不完全确定为什么会按照这个特定顺序进行,但您可以看到,B的初始化不会被中断以初始化A的原因是A的初始化已经开始。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接