C#中静态类初始化的顺序是否是确定性的?

50

我进行了一些搜索,我认为以下代码保证会产生输出:

B.X = 7

B.X = 0

A.X = 1

A = 1, B = 0
static class B
{
    public static int X = 7;
    static B() {
        Console.WriteLine("B.X = " + X);
        X = A.X;
        Console.WriteLine("B.X = " + X);
    }
}

static class A
{
    public static int X = B.X + 1;
    static A() {
        Console.WriteLine("A.X = " + X);
    }
}

static class Program
{
    static void Main() {
        Console.WriteLine("A = {0}, B = {1}", A.X, B.X);
    }
}

我已经多次运行了这个代码,并且每一次都得到了上面代码部分之外的输出结果;我只是想确认一下,即使在文本上类 A 和类 B 被重新排列,它们也会发生变化吗?

第一次使用静态对象时是否保证会触发静态成员的初始化,随后实例化其静态构造函数?对于这个程序,在主函数中使用 A.X 将触发 A.X 的初始化,然后初始化 B.X,接着初始化 B(),等完成 A.X 的初始化后,将会继续初始化 A(),最后,Main() 函数将输出 A.XB.X


1
如果Main()先使用了B.X,那么输出的值将会是7、8、8,A=8,B=8。 - Jess
2
对于你问题中给出的特定代码,Porges 的答案是完全正确的。对于你标题中更一般的问题——“C# 中静态类初始化的顺序是否确定?”——答案是取决于情况:如果涉及到的静态类型都有静态构造函数,则初始化顺序是确定的;否则就不是。 - LukeH
相关:https://dev59.com/-nI_5IYBdhLWcg3wK_zE - Ohad Schneider
4个回答

60
直接引用ECMA-334:
17.4.5.1:“如果一个类中存在静态构造函数(§17.11),则在执行该静态构造函数之前立即执行静态字段初始化器。否则,在该类的任何静态字段首次使用之前的某个实现相关时间执行静态字段初始化器。”
17.11:触发静态构造函数的执行是在应用程序域内发生以下事件中的第一个事件:
- 创建该类的实例。 - 引用该类的任何静态成员。
如果一个类包含Main方法(§10.1),那么在调用Main方法之前,该类的静态构造函数将被执行。如果一个类包含任何具有初始值设定项的静态字段,则这些初始化器在执行静态构造函数(§17.4.5)之前按照文本顺序执行。
因此,顺序如下:
- 使用A.X,因此调用static A()。 - A.X需要初始化,但它使用了B.X,所以调用static B()。 - B.X需要被初始化,并且被初始化为7。B.X = 7。 - 所有B的静态字段都已被初始化,因此调用static B()。X被打印为"7",然后设置为A.X。A已经开始被初始化,因此我们得到了A.X的默认值(“当初始化一个类时,该类中的所有静态字段首先被初始化为它们的默认值”);B.X = 0,并被打印为"0"。
  • 初始化B完成,A.X的值设置为B.X+1A.X = 1
  • A所有静态字段初始化完成,所以调用了static A()方法,并打印出A.X的值("1")。
  • 回到Main方法中,打印出A.XB.X的值("1","0")。
  • 实际上,Java标准文档中有这样的注释:

    17.4.5: 变量初始化器中的静态字段可能会被观察到处于默认值状态。但是,出于风格方面的考虑,强烈不建议这样做。


    初始化程序不应该在构造函数之前被调用吗?所以第一步是正确的吗?“使用了A.X,因此调用静态A()。”如果类中存在静态构造函数(§17.11),则在执行该静态构造函数之前立即执行静态字段初始化程序。 - bobbyalex

    13
    C#规范中涉及了四个不同的规则来保证这一点,而且它是特定于C#的。.NET运行时所做的唯一保证是类型初始化在使用之前开始。
    以下是涉及到的四个规则:
    1. 静态字段在类型初始化器运行之前被零初始化。 2. 静态字段初始值设定项在静态构造函数之前立即运行。 3. 静态构造函数在第一个实例构造函数调用或第一个静态成员引用时被调用。 4. 函数参数按从左到右的顺序进行评估。
    依赖此方法是一个非常糟糕的想法,因为它很可能会让阅读您代码的任何人感到困惑,特别是如果他们熟悉具有类似语法但未提供以上所有保证的语言。
    请注意,Porges的评论与我的初始陈述(基于.NET行为)相关,即保证过于薄弱,无法确保观察到的行为。Porges正确指出了保证足够强大,但实际上涉及到的链比他建议的要复杂得多。

    4
    不正确。请参阅17.4.5.1:“[如果类中存在静态构造函数(§17.11),则在执行该静态构造函数之前立即执行静态字段初始化程序。]否则,静态字段初始化程序将在该类的静态字段首次使用之前的实现定义时间执行。” 只有当您没有静态构造函数时,静态字段初始化才是实现定义的。 - porges

    6

    您可能会感兴趣的是,在默认初始化和变量初始化之间,甚至可以为字段分配值。

    private static int b = Foo();
    private static int a = 4;
    
    private static int Foo()
    {
        Console.WriteLine("{0} - Default initialization", a);
        a = 3;
        Console.WriteLine("{0} - Assignment", a);
        return 0;
    }
    
    public static void Main()
    {
        Console.WriteLine("{0} - Variable initialization", a);
    }
    

    输出

    0 - Default initialization
    3 - Assignment
    4 - Variable initialization
    

    -2

    静态成员的确定性初始化确实是有保障的...但不是按照“文本顺序”进行的。此外,它可能不是完全懒惰地执行(仅在首次引用静态变量时才执行)。然而,在使用整数的示例中,这不会有任何影响。

    在某些情况下,希望获得懒惰初始化(尤其是对于昂贵的单例)-这种情况下,您有时必须 费一些心思 才能正确地实现它。


    1
    由于AB的初始化之间存在循环引用,因此懒惰性确实会影响输出。 - Ben Voigt
    3
    实际上,静态字段初始化器按照文本顺序执行:stackoverflow.com/a/3681278/616827 - Jeff

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