静态变量初始化顺序的歧义性

6
在研究如何在C#中构建单例模式的最佳方法时,我偶然发现了这篇文章,其中简要提到在C++中存在以下问题:

"C++规范在静态变量初始化顺序方面留下了一些模棱两可的地方。"

于是我查看了此处此处。基本上(就我所理解的)是指C++中静态变量的初始化顺序是未定义的。好吧,我想到这里应该还算顺利,但接下来我想弄明白文章后面提到的这个陈述。

"幸运的是,.NET Framework通过其处理变量初始化的方式解决了这种不确定性。"

于是我找到了这个页面,他们在其中提到:

类的静态字段变量初始化器对应于按它们在类声明中出现的文本顺序执行的一系列赋值。

并给出以下示例:
using System;
class Test
{
   static void Main() {
      Console.WriteLine("{0} {1}", B.Y, A.X);
   }
   public static int F(string s) {
      Console.WriteLine(s);
      return 1;
   }
}
class A
{
   static A() {}
   public static int X = Test.F("Init A");
}
class B
{
   static B() {}
   public static int Y = Test.F("Init B");
}

the output must be: 
Init B 
Init A
1 1

"由于静态构造函数执行的规则(在第10.11节中定义),B的静态构造函数(以及B的静态字段初始化程序)必须在A的静态构造函数和字段初始化程序之前运行。" 但是我困惑的是,我的理解是,这些示例中静态变量的初始化顺序将基于首次调用类中方法或字段的时间,这又取决于代码块的执行顺序(本例从左到右)。换句话说:完全独立于类声明的位置或顺序。然而,根据我对该文章的解释,它说这是由于这些类的声明顺序,而我的测试并不支持这一点?请问有人能为我澄清这一点(以及文章试图表达的观点),并提供更好的例子来说明所描述的行为吗?"
2个回答

8

一个类的静态字段变量初始化器对应着一系列按照它们在类声明中出现的文本顺序执行的赋值语句。

这意味着在同一个类中,静态字段按照它们在源代码中出现的顺序进行初始化。例如:

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

当静态字段初始化时,保证XY之前被初始化。
“因为静态构造函数执行的规则(如第10.11节所定义)要求B的静态构造函数(以及B的静态字段初始化程序)必须在A的静态构造函数和字段初始化程序之前运行。”
这意味着,当访问这些类的表达式出现¹时,每个类的静态构造函数和成员初始化将按照评估顺序运行。源代码中类定义的相对出现顺序不起任何作用,即使它们出现在同一个源文件中(它们肯定没有义务这样做)。例如:
static void Main() {
    Console.WriteLine("{0} {1}", B.Y, A.X);
}

假设 AB 都没有被静态初始化,按照评估顺序的规定,B 的所有字段将在任何 A 字段之前被初始化。每个类的字段将按照第一条规则指定的顺序进行初始化。
¹ 为了讨论方便,我忽略了 beforefieldinit 的存在。

非常感谢您提供如此简洁明了的答案。 - Maxim Gershkovich

3
在C++中,单个翻译单元中具有静态存储期的变量的初始化顺序是定义这些变量的顺序。对于具有静态存储期的变量的初始化顺序跨不同的翻译单元是未指定的。
也就是说,C++标准确实提供了类似于您引用的保证,将类中的声明顺序替换为定义这些变量的单个翻译单元中的顺序。但这并不是重要的区别。
虽然在C++中这是唯一的保证,但在C#中还有额外的保证,即所有静态成员将在第一次使用该类之前初始化。这意味着,如果您的程序依赖于A(考虑每种类型在不同的程序集中的情况),它将启动A中所有静态字段的初始化,如果A反过来依赖于B进行任何这些静态初始化的操作,则会在那里触发B静态成员的初始化。
与此相比,C++在静态初始化期间假定所有其他具有静态持续时间的变量都已初始化[*]。这是主要的区别:C++假设它们已经初始化,C#确保在使用之前初始化它们。

[*]从技术上讲,这种情况在标准中可能是动态初始化。在每个翻译单元中具有静态存储期的变量的初始化是一个两步过程,其中在第一次传递期间,静态初始化将变量设置为固定的常量表达式,稍后在称为动态初始化的第二个传递中,将初始化所有静态存储的变量,其初始化程序不是常量表达式。


+1 和一个小问题:严格来说,如果一个类型被装饰为 beforefieldinit,那么 CLR 不需要在实际访问静态成员之前初始化它们(例如,您可以随意调用静态方法)。实际上,在类被访问之前就已经初始化了这些字段。 - Jon

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