在其静态构造函数中创建类的实例 - 为什么允许它?

时间:2010-09-02 18:30:51

标签: c#

关于SO的另一个问题激发了我在C#中试用这段代码:

class Program
{
    static Program() 
    {
        new Program().Run();
    }

    static void Main(string[] args) { }

    void Run()
    {
        System.Console.WriteLine("Running");
    }
}

运行时打印“正在运行”。

我实际上期望编译器抱怨这个。毕竟,如果该类尚未被静态构造函数初始化;我们怎样才能确定在它上面调用方法是否有效?

那么为什么编译器不会限制我们这样做呢?这有什么重要的使用场景吗?

修改

我知道Singleton模式;问题的关键是为什么我可以在静态构造函数完成之前调用实例上的方法。到目前为止,JaredPar的回答有一些很好的理由。

4 个答案:

答案 0 :(得分:6)

略有不同的问题。

  

编译器如何阻止您这样做?

当然,在您的样本中检测非常容易,但这个样本怎么样?

class Program {
  static void Fun() {
    new Program(); 
  }
  static Program() {
    Fun();
  }
}

你可以欺骗编译器允许这样做的方式实际上是无止境的。即使编译器得到了所有的答案,你仍然可以用反射来击败它。

最后虽然这实际上是合法的,但如果有点危险,则在C#和IL中都有代码。只要你小心从这段代码中访问静态,这样做是安全的。对于像Singleton的

这样的某些模式,它也是有用的/可能是必要的

答案 1 :(得分:6)

这是允许的,因为不允许它会更糟糕的很多。像这样的代码会严重陷入僵局:

class A {
    public static readonly A a;
    public static readonly B b;
    static A() {
        b = new B();
        a = B.a;
    }
}

class B {
    public static readonly A a;
    public static readonly B b;
    static B() {
        a = new A();
        b = A.b;
    }
}

你当然指着一把装满枪的脚。

CLI规范(Ecma 335)分区II,第10.5.3.2节“宽松保证”中记录了此行为:

可以使用属性beforefieldinit(第10.1.6节)标记类型,以指示§10.5.3.1中指定的保证不一定是必需的。特别是,不需要提供上面的最终要求:在调用或引用静态方法之前,不需要执行类型初始化程序。

[原理:当代码可以在多个应用程序域中执行时,确保最终保证会变得特别昂贵。同时,对大量托管代码的检查表明,很少需要这种最终保证,因为类型初始化器几乎总是简单的初始化方法 静态字段。将其留给CIL发电机(因此,可能还有程序员)决定是否需要这种保证,因此在需要时以一致性保证为代价提供效率。
最终理由]

C#编译器确实在类上发出 beforefieldinit 属性:

.class private auto ansi beforefieldinit ConsoleApplication2.Program
       extends [mscorlib]System.Object
{
   // etc...
}

答案 2 :(得分:0)

静态构造函数只能初始化静态类成员,这与类实例和常规非静态类成员无关。

答案 3 :(得分:0)

你可能没有意识到,对于没有非静态构造函数的每个类,编译器都会生成一个。这与你的静态构造函数不同,当你把它归结为MSIL时,它只不过是一个告诉CLR的标志“嘿,在你运行main()之前运行这段代码”。因此,首先执行静态构造函数的代码。它使用在幕后生成的NON-static构造函数实例化本地作用域的Program对象,并且一旦实例化,就在对象上调用Run()。然后,因为您没有将此新对象存储在任何位置,所以在构造函数完成执行时将其处理掉。然后main()函数运行(并且什么都不做)。

尝试此扩展:

class Program
{
    static Program() 
    {
        new Program().Run();
    }

    public Program()
    {
        Console.WriteLine("Instantiating a Program");
    }

    public override void Finalize()
    {
        Console.WriteLine("Finalizing a Program");
    }

    static void Main(string[] args) { Console.WriteLine("main() called"); }

    void Run()
    {
        System.Console.WriteLine("Running");
    }
}

查看输出结果。我的猜测是它看起来像这样:

Instantiating a Program
Running
Finalizing a Program
main() called

最后两行可能会被交换,因为垃圾收集可能无法在主开始运行之前销毁实例(GC在单独的托管线程中运行,因此它在进程的生命周期内按照自己的时间工作) ,但实例是范围内静态构造函数的本地实例,因此在main()开始运行之前标记为集合。因此,如果在打印消息之前在main()中调用了Thread.Sleep(1000),那么GC应该在那个时间内收集对象。