static constructors和BeforeFieldInit?

如果类型没有静态构造函数,则字段初始值设定项将在使用类型之前执行 – 或者在运行时突发奇想之前的任何时间执行

为什么这个代码:

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的顺序是可以理解的。 但为什么处理static methodstatic field 不同

我的意思是为什么起始行和结束行在静态方法与静态字段的不同位置? 我的意思是 – 在这两种情况下他都要初始化那些领域……为什么呢?

(我知道我可以添加静态ctor,使其相同 – 但我问这个特殊情况。)

(ps Dump()就像console.write)

释放JIT的行为(从4.0 IIRC)不运行静态初始化程序,除非您调用的方法触及静态字段。 这可能意味着静态字段初始化。 如果我在调试器之外的发行版中运行你的第一个代码,我得到:

 -------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等)之间发生变化
  • 它可以根据是否附加调试器以及它是否是调试/发布版本而更改

无法保证调用静态构造函数的时间,因此对于程序来说,它就像C ++中的Undefined Behavior。 没有人应该依赖静态构造函数调用的顺序。 例如,如果您在发布时编译程序,您将看到在两种情况下同时调用静态counstructor。

这是使用.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, 

它会看到代码中引用了哪些类型。如果任何类型定义了类型构造函数,JIT编译器会检查是否已经为此AppDomain执行了类型的类型构造函数。如果构造函数从未执行过,则JIT编译器会发出一个调用将类型构造函数转换为JIT编译器正在发出的本机代码。

@评论

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

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

结果与上述IL相同。 所以你自己的类型构造函数和编译器生成的构造函数之间基本没有区别(反正只能有一个)。