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-------

a和的顺序b是可以理解的。但为什么处理 static method不同于. static field

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

(我知道我可以添加静态 ctor 使其相同 - 但我问的是这种特殊情况。)

(ps Dump() 就像 console.write)

4

3 回答 3

6

发布 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 等)之间发生变化
  • 它可以根据是否附加调试器以及它是否是调试/发布版本而改变
于 2012-10-10T10:08:43.283 回答
3

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

于 2012-10-10T10:09:21.790 回答
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,

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

@评论

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

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

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

于 2012-10-10T10:32:22.230 回答