静态构造函数和BeforeFieldInit是什么?

5
如果一个类型没有静态构造函数,字段初始化器会在该类型被使用之前执行 - 或者在运行时任意更早的时候执行。
为什么这段代码:
void Main()
{ 
  "-------start-------".Dump();
   Test.EchoAndReturn("Hello");
  "-------end-------".Dump();

}

 class Test
{
    public static string x = EchoAndReturn ("a");
    public static string y = EchoAndReturn ("b");
    public static string EchoAndReturn (string s)
    {
        Console.WriteLine (s);
        return s;
    }
}

产出:
-------start-------
a
b
Hello
-------end-------

当这段代码:

void Main()
{ 
  "-------start-------".Dump();
   var test=Test.x;
  "-------end-------".Dump();

}

产出率
a
b
-------start-------
-------end-------
ab的顺序是可以理解的,但为什么处理静态方法静态字段不同呢?
我的意思是为什么静态方法和静态字段的起始结束行不同?我是说,在这两种情况下,他都必须初始化这些字段...那么为什么?
(我知道我可以添加静态构造函数使它们相同 - 但我正在询问这个特定的情况。)
(附注:Dump()就像console.write)

2
请注意,如果我浏览您的问题,这可以为人力资源招聘打下良好的基础 :) - Tigran
1
@Tigran 是的,我知道。我总是喜欢挑战难度较高的问题。不要问我为什么。(你可以确定这种问题并不在我的工作范围之内) :-) - Royi Namir
1
我在4.0到2.0的版本上尝试了这个,行为是一样的。如果这是一个错误,那么它存在的时间太长了... - Tigran
1
提供信息,我认为Linqpad在这里充当了调试器的角色,这意味着它具有副作用,并且实际上正在改变结果。对于稳健的测试,您可能需要使用控制台exe。 - Marc Gravell
这是一个经典的误解:规范并不保证最佳的惰性,它只是允许惰性。两者之间有很大的区别。基本上,惰性的确切程度取决于实现。 - AnorZaken
显示剩余4条评论
3个回答

6

自4.0版本起,发行版JIT的行为是:只有当您调用的方法涉及到静态字段时,才会运行静态初始化程序。这意味着静态字段未被初始化。如果我在调试器之外的发行版中运行您的第一个代码,我会得到:

-------start-------
Hello
-------end-------

如果我使用调试器运行它(发布版本),或者在调试版本中运行它(带有或不带有调试器附加),我会得到:

-------start-------
a
b
Hello
-------end-------

到目前为止,很有趣。为什么会这样呢:
a
b
-------start-------
-------end-------

看起来在这种情况下,每个方法的JIT本质上负责运行静态构造函数。通过添加以下代码,您可以看到这一点:

if(NeverTrue()) { // method that returns false
        "-------start-------".Dump();
        var test = Test.x;
        "-------end-------".Dump();
}

这将会打印(即使在没有调试器的发布版本中)。

a
b

因此,访问字段的可能性非常重要。如果我们将Test.x更改为调用一个不访问字段的方法(并删除NeverTrue()),那么我们将根本没有任何输出
因此,在某些版本的CLI中,静态初始化程序的执行可能被推迟到包含对任何字段的提及的方法的JIT步骤中(它不会检查该字段是否具有初始化程序)。
只要我们不触及静态字段,我们甚至可以创建对象实例而不运行静态初始化程序:
 public Test()
 {
     a = "";
 }
 string a;

使用:

"-------start-------".Dump();
new Test();
"-------end-------".Dump();

仅输出(无调试器):

-------start-------
-------end-------

然而!我们不应该构建任何依赖于以下时间的东西:

  • 它在.NET版本之间发生变化
  • 它可能会在平台之间发生变化(x86,x64,CF,SL,.NETCore等)
  • 它可以根据调试器是否附加以及它是调试/发布版本而发生变化

所以整个 beforeinitField 需要在调试/发布版本中有不同的考虑? - Royi Namir
1
通过以不同的方式运行控制台 exe,答案似乎是肯定的。 - Marc Gravell
该规范基本上只是说明这些字段将在实现依赖的时间被初始化,然后再被使用。需要注意的关键点是它并不保证推迟初始化到需要时才执行。它可以在进入Main()方法之前运行所有类中的静态字段初始化器,并且仍然符合规范。该规范没有强制执行懒加载,只是允许它的存在。 - AnorZaken

3
静态构造函数被调用的时间不是保证的,所以对于程序来说,就像C++中的未定义行为一样。没有人应该依赖静态构造函数调用的顺序。例如,如果您在发布下编译程序,您将看到静态构造函数在两种情况下都在相同的时间被调用。

并不完全是这样。C#规范更加严格,在大多数情况下,您可以对结果做出各种保证。假设您以某种方式封装和初始化,规范足够强大,可以保证确切的结果。基本上,在C#中,您无法确定在一般情况下会发生什么,但是您可以设计静态初始化的方式,以创建强有力的保证。而在C++中,它只是一个未定义的纯幸运的鼠窝。(C#规范有示例,显示如果存在循环依赖项,则应该期望什么结果。) - AnorZaken

1

这是使用 .NET 4.0

如果一个类型没有静态构造函数但有初始化的静态字段,编译器会创建一个类型构造函数并将初始化放在其中。

class Test
    {
        public static string x = EchoAndReturn("a");
        public static string y = EchoAndReturn("b");
        public static string EchoAndReturn(string s)
        {
            Console.WriteLine(s);
            return s;
        }
    }

结果在以下 IL 中(只有 cctor 部分)

.method private hidebysig specialname rtspecialname static 
        void  .cctor() cil managed
{
  // Code size       31 (0x1f)
  .maxstack  8
  IL_0000:  ldstr      "a"
  IL_0005:  call       string ConsoleApplication1.Test::EchoAndReturn(string)
  IL_000a:  stsfld     string ConsoleApplication1.Test::x
  IL_000f:  ldstr      "b"
  IL_0014:  call       string ConsoleApplication1.Test::EchoAndReturn(string)
  IL_0019:  stsfld     string ConsoleApplication1.Test::y
  IL_001e:  ret
} // end of method Test::.cctor

此外,根据《CLR via C#》的说法,JIT编译器会预先检查每个方法,了解哪些类型具有静态构造函数。如果尚未调用静态构造函数,则JIT编译器将调用它。
这就解释了两个代码片段之间的差异。
When the just-in-time (JIT) compiler is compiling a method,

it sees what types are referenced in the code .If any of the types define a type constructor, the JIT compiler checks if the type’s type constructor has already been executed for this AppDomain .If the constructor has never executed, the JIT compiler emits a call to the type constructor into the native code that the JIT compiler is emitting .

@注释

如果您将一个字段初始化器移动到用户定义的类型构造函数中,编译器也会将您在类级别上初始化的另一个字段移动到类型构造函数中。

static Test()
{
  y = EchoAndReturn("b");
}

得到的中间语言与上面的相同。因此,您自己的类型构造函数和编译器生成的函数基本上没有区别(反正只能有一个)。


那么它创建了一个“假”的构造函数?如果是这样,那么创建自己的构造函数与使用现成的构造函数有什么区别? - Royi Namir
@RoyiNamir添加了一个回复。不确定您提到的“ready ctor”是什么意思。另外请注意,静态构造函数是“cctor”,而实例构造函数是“ctor”。 - Alex

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