调用静态构造函数和实例构造函数

6
据我所知,父类的构造函数会先被调用,然后才会调用子类的构造函数。但是为什么在静态构造函数的情况下,它会先从派生类执行,然后再从子类执行呢?
namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            Child t = new Child();
        }
    }
    
    class Parent
    {
        public Parent()
        {
            Console.WriteLine("Parent Instance Constructor");
            Console.ReadKey();
        }
        static Parent()
        {
            Console.WriteLine("Parent Static Constructor");
            Console.ReadKey();
        }
    }
    class Child : Parent
    {
        public Child()
        {
            Console.WriteLine("Child Instance Constructor");
            Console.ReadKey();
        }

        static Child()
        {
            Console.WriteLine("Child Static Constructor");
            Console.ReadKey();
        }
    }
}

输出:

子类静态构造函数

父类静态构造函数

父类实例构造函数

子类实例构造函数

现在根据 Jeppe Stig Nielsen 的建议,当我在构造函数中初始化静态字段时,它的执行顺序如下:

输出:

父类静态构造函数

子类静态构造函数

父类实例构造函数

子类实例构造函数

class XyzParent
{
    protected static int FieldOne;
    protected int FieldTwo;

    static XyzParent()
    {
        // !  
        FieldOne = 1;
        Console.WriteLine("parent static");
    }
    internal XyzParent()
    {
        // !  
        FieldOne = 10;
        // !  
        FieldTwo = 20;
        Console.WriteLine("parent instance");
    }
}

class XyzChild : XyzParent
{
    static XyzChild()
    {
        // !  
        FieldOne = 100;
        Console.WriteLine("child static");
    }
    internal XyzChild()
    {
        // !  
        FieldOne = 1000;
        // !  
        FieldTwo = 2000;
        Console.WriteLine("child instance");
    }
}

为什么会出现这种矛盾的行为?

2
如果您给类Parent添加两个字段,一个是静态字段,一个是实例字段,那么这将更有趣。然后构造函数应该分配给这些字段。静态构造函数只能分配给静态字段,但实例构造函数可以分配给两个字段。那么构造函数会按照相同的顺序运行吗? - Jeppe Stig Nielsen
如果你在代码中增加更多的Console.WriteLines,它会帮助你理解。我会更新你的代码,告诉你在哪里增加Console.WriteLines。在我添加了// !的地方加入Console.WriteLines,阅读我的答案,重新运行代码,看看你是否现在明白了这里发生了什么。这里并没有矛盾。 - Eric Lippert
3个回答

24

首先,这种行为并不矛盾;它与规则是一致的。您只是不知道规则是什么。

您应该阅读我关于实例构造函数的两部分系列和关于静态构造函数语义的四部分系列文章。它们从这里开始:

http://blogs.msdn.com/b/ericlippert/archive/2008/02/15/why-do-initializers-run-in-the-opposite-order-as-constructors-part-one.aspx

和这里:

http://ericlippert.com/2013/02/06/static-constructors-part-one/

分别。

这些文章应该清楚地回答了您的问题,但以防万一不是100%清楚,让我总结一下。相关规则如下:

  • 第一条规则:在访问任何静态字段、执行任何静态方法和执行任何实例构造函数之前,静态构造函数都会运行。
  • 第二条规则:派生类实例构造函数在运行派生类实例构造函数体之前会调用基类实例构造函数。

所以,当您执行new Child()时会发生什么?

  • 应用第一条规则。我们即将调用Child的实例构造函数,因此必须先调用Child的静态构造函数。因此它最先运行。
  • 在Child的静态构造函数返回后,Child的实例构造函数运行。适用第二条规则:在运行其主体之前,Child实例构造函数要做的第一件事是运行Parent的实例构造函数。
  • 再次应用第一条规则。我们即将调用Parent的实例构造函数,因此必须先调用Parent的静态构造函数。因此它运行。
在父类的静态构造函数返回后,父类的实例构造函数会运行。Rule Two适用于此情况:它调用对象的实例构造函数,该函数不执行任何有趣的操作,然后运行父类的实例构造函数体。
控制权返回到子类的实例构造函数,并运行其主体。
因此,顺序是Child的静态构造函数,然后是Parent的静态构造函数,接着是Parent的主体,最后是Child的主体。
现在让我们看看您的第二个示例。当您说“new XyzChild”时会发生什么?
Rule One适用。我们即将调用XyzChild的实例构造函数,因此首先调用XyzChild的静态构造函数。它的主体开始执行,然后...
...再次应用Rule One。我们即将访问XyzParent的静态字段,因此必须执行XyzParent的静态构造函数。
XyzParent的静态构造函数执行。它访问一个字段,但是静态构造函数已经在此线程上运行,因此不会递归触发静态构造函数。它打印它在父类中。
控制权返回到子类的静态构造函数,它打印出它在子类中。
现在可以运行子类的实例构造函数。Rule Two适用,XyzParent的实例构造函数首先运行。
Rule One应用,但是XyzParent的静态构造函数已经运行,因此跳过它。
执行XyzParent的实例构造函数体并将控件返回到XyzChild的静态构造函数。
运行XyzChild的实例构造函数体。
因此,这里没有任何不一致;两个规则都被正确应用。

当然。所以在最后一种情况下,XyzChild的静态构造函数的初始部分首先运行,然后构造函数的执行被“暂停”,同时运行整个XyzParent的静态构造函数,只有在此完成后才会返回控制权到XyzChild的静态构造函数,继续运行其余部分。 - Jeppe Stig Nielsen
1
@JeppeStigNielsen:当你调用另一个方法时,它与每个方法都以完全相同的方式“暂停”;返回地址和新的激活帧被推入堆栈,新方法被调用,当它返回时,激活帧被弹出,原始方法在返回地址处继续执行。除了你调用的方法的调用位置不会出现在源代码中之外,没有任何魔法。 - Eric Lippert

6

静态构造函数总是在非静态构造函数之前执行。当第一次访问类时,将调用静态构造函数。

根据MSDN文档,

  • 静态构造函数不带访问修饰符或参数。
  • 在创建第一个实例或引用任何静态成员之前,自动调用静态构造函数以初始化类。
  • 不能直接调用静态构造函数。用户无法控制程序何时执行静态构造函数。
  • 静态构造函数的典型用途是当类使用日志文件时,构造函数用于向该文件写入条目。
  • 在创建包装器类以处理非托管代码时,静态构造函数也很有用,此时构造函数可以调用LoadLibrary方法。
  • 如果静态构造函数引发异常,则运行时不会再次调用它,并且类型将在应用程序域的生存期内保持未初始化状态。

1
但为什么子类的静态构造函数会比实例构造函数先执行呢? - F11
一个静态构造函数会在创建第一个实例或引用任何静态成员之前自动调用以初始化类。 Child 先被创建。Parent类不会被创建,只是被继承。 - John Woo
1
但是,在我看来,使用上述代码调用这两个“静态”构造函数的顺序并不明确定义。然而,如果这两个类的静态构造函数都访问了“Parent”类的某些静态字段,那么我相信这些静态构造函数将按照一定的顺序运行,并且这个顺序与原始帖子中得到的顺序不同。 - Jeppe Stig Nielsen
@JeppeStigNielsen,您所说的“..are called is not well-defined with the above code.”是什么意思?Parent类并没有被创建,它只是被继承了。Child类被创建了,这就是为什么它输出Child Static Constructor,但在调用子类的非静态构造函数之前,它会先初始化继承的类,在这种情况下是Parent类。 - John Woo
我不知道。运行时可能会说:“好的,我预测在不久的将来我需要运行Parent静态构造函数,所以让我先这样做。现在,当然,我还必须运行Child静态构造函数,并且我觉得现在是个好时机。然后,代码说Child t = new Child();,现在我可以运行那部分(实例构造函数)。”如果它这样做,我认为是允许的。用户无法控制程序何时执行静态构造函数(引用您的语句)。请参见我的答案,情况略有不同。 - Jeppe Stig Nielsen
@JeppeStigNielsen:在给定的两个代码片段中,静态构造函数执行的顺序是明确定义的。有关详细信息,请参阅我的答案。只有当存在具有初始值设定项但没有静态构造函数时(或者当顺序依赖于字段词法顺序而没有词法顺序因为字段位于部分类的两半时),才会变成实现定义。 - Eric Lippert

2

在您的情况下,静态构造函数运行的顺序是未定义的(我认为)。唯一可以保证的是,在实例创建之前它们将会运行。

我将您的代码更改为:

    class XyzParent
    {
        protected static int FieldOne;
        protected int FieldTwo;

        static XyzParent()
        {
            FieldOne = 1;
            Console.WriteLine("parent static");
        }
        internal XyzParent()
        {
            FieldOne = 10;
            FieldTwo = 20;
            Console.WriteLine("parent instance");
        }
    }
    class XyzChild : XyzParent
    {
        static XyzChild()
        {
            FieldOne = 100;
            Console.WriteLine("child static");
        }
        internal XyzChild()
        {
            FieldOne = 1000;
            FieldTwo = 2000;
            Console.WriteLine("child instance");
        }
    }

现在运行的顺序更加重要,因为它们写入同一字段。使用我的代码版本,输入new XyzChild();将导致以下输出:

parent static
child static
parent instance
child instance

编辑:Eric Lippert的回答给出了更精确的解释。上面的代码只在静态构造函数的末尾执行WriteLine。在静态构造函数的开头添加额外的WriteLine,可以看到XyzParent静态构造函数在执行XyzChild静态构造函数的过程中被“插入”执行。


但为什么会出现这种行为?我无法理解。你能解释一下吗? - F11
1
@little 你有没有尝试阅读《C#语言规范》中的“静态构造函数”一节?在我的电脑上,它位于'C:\Program Files (x86)\Microsoft Visual Studio 11.0\VC#\Specifications\1033'文件夹中(根据版本和区域设置,数字可能会有所不同)。阅读完后,我不确定我之前说的顺序是否正确。更多的是关于Childstatic构造函数是否引用了Parent的成员。如果没有(就像你的例子),它可以先运行。如果有(我的例子),则Parent的构造函数会先运行。 - Jeppe Stig Nielsen
1
静态构造函数的运行顺序在两个程序中都是明确定义的。 - Eric Lippert

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