zoukankan      html  css  js  c++  java
  • [C#解惑] #2 对象的初始化顺序

    谜题

    上一篇C#解惑中,我们提到了对象的初始化顺序。当我们创建一个子类的实例时,总是会先执行基类的构造函数,然后再执行子类的构造函数。那么实例字段是什么时候初始化的呢?静态构造函数和静态字段呢?今天我们就来研究一下这个话题。

    我们先来看这样一段代码:

    class Foo
    {
        public Foo(string s)
        {
            Console.WriteLine(s);
        }
        public void Bar() { }
    }
    class Base
    {
        readonly Foo baseFoo1 = new Foo("Base initializer");
        static readonly Foo baseFoo2 = new Foo("Base static initializer");
        static Base()
        {
            Console.WriteLine("Base static constructor");
        }
        public Base()
        {
            Console.WriteLine("Base constructor");
        }
    }
    class Derived : Base
    {
        readonly Foo derivedFoo1 = new Foo("Derived initializer");
        static readonly Foo derivedFoo2 = new Foo("Derived static initializer");
        static Derived()
        {
            Console.WriteLine("Derived static constructor");
        }
        public Derived()
        {
            Console.WriteLine("Derived constructor");
        }
    }
    static class Program
    {
        static void Main()
        {
            new Derived();
            Console.Read();
        }
    }
    

    猜一猜它的输出结果是什么?如果猜不出来,就运行一下看看吧。

    Derived static initializer
    Derived static constructor
    Derived initializer
    Base static initializer
    Base static constructor
    Base initializer
    Base constructor
    Derived constructor
    

    是不是有点出乎你的意料?没关系,我们来一步一步解释。

    解惑

    上期已经介绍了构造函数的初始化顺序,所以这次略过不谈,直接来看看实例成员的初始化器。一般来说,我们在构造一个类型的实例时,会先初始化成员,然后初始化构造函数(编译器会把初始化成员的代码编译到构造函数代码的最顶部)。但初始化一个子类的时候,父类的成员、构造函数的初始化,和子类的成员、构造函数的初始化顺序是什么样的呢?

    实例初始化器和实例构造函数的执行顺序

    我们把上面的代码简化一下,去掉静态构造函数和静态初始化器。

    class Base
    {
        readonly Foo baseFoo = new Foo("Base initializer");
        public Base()
        {
            Console.WriteLine("Base constructor");
        }
    }
    class Derived : Base
    {
        readonly Foo derivedFoo = new Foo("Derived initializer");
        public Derived()
        {
            Console.WriteLine("Derived constructor");
        }
    }
    

    结果如下所示:

    Derived initializer
    Base initializer
    Base constructor
    Derived constructor
    

    这可能会有点出乎你的意料,因为直观上来说,似乎应该是先初始化父类的成员和构造函数,再初始化子类的成员和构造函数:

    Base Initializers
    Base Constructor
    Derived Initializers
    Derived Constructor
    

    但实际上为什么会先初始化子类的成员呢?这是因为,按照这样的初始化顺序,所有引用类型的只读字段(注意这里的readonly并不是随手写写的)都能确保在调用时不为null。而如果先初始化基类的成员和构造函数,就无法给出这样的保证。

    比如下面的代码:

    internal class Base
    {
        public Base()
        {
            Console.WriteLine("Base constructor");
            if (this is Derived) (this as Derived).N();
            // would deref null if we are constructing an instance of Derived
            M();
            // would deref null if we are constructing an instance of MoreDerived
        }
    
        public virtual void M()
        {
        }
    }
    internal class Derived : Base
    {
        private readonly Foo derivedFoo = new Foo("Derived initializer");
    
        public void N()
        {
            derivedFoo.Bar();
        }
    }
    internal class MoreDerived : Derived
    {
        public override void M()
        {
            N();
        }
    }
    

    如注释所示,在构造Derived类型的实例时,如果先初始化Base的构造函数,后初始化Derived的成员,那么在Base的构造函数中调用DerivedN时,derivedFoo就会为null,因为它还没有初始化。试想一下,你正在调用一个对象的方法,但这个对象的字段没有初始化,构造函数也还没有执行,这显然是不合理的。

    同样,在构造MoreDerived时,在Base的构造函数中调用M(进而调用N)也会得到空引用,因为DerivedderivedFoo仍然没有初始化。

    注意 尽管类似if (this is Derived) (this as Derived).N();这样的代码是合法的,但是一定注意不要这样写。在基类的构造函数中,把“自己”转换为自己的子类,想想都不可思议……

    因此,类型的初始化顺序必须是这样的:

    Derived initializer
    Base initializer
    Base constructor
    Derived constructor
    

    静态初始化器和静态构造函数的初始化顺序

    我们都知道,静态构造函数是一个特殊的构造函数,它在该类型的所有成员(包括实例构造函数)第一次被访问之前执行。而与实例的初始化器会在实例构造函数之前执行类似,静态初始化器会在静态构造函数之前执行。结合这两点,我们来看看本文最初的谜题。在执行new Derived()时,是第一次访问Derived类,此时会率先执行它的静态构造函数,而在执行静态构造函数之前,会执行静态初始化器。因此打印的结果应该为:

    Derived static initializer
    ...
    Derived static constructor
    ...
    Derived constructor
    

    现在问题来了,基类的静态构造函数会被执行吗?如果会,是在什么时候执行的呢?会和实例构造函数一样,在子类的静态初始化器之后吗?

    Derived static initializer
    Base static initializer
    Base static constructor
    Derived static constructor
    

    稍加思考我们就能得出答案。由于静态初始化器和静态构造函数都是静态的,所以在执行的时候并不会出发基类的任何行为(记住我们前面说的,只有当类的成员被调用的时候,才会执行静态初始化器和静态构造函数)。因此在它们之后应该继续执行子类的实例初始化器。而在这之后,按顺序该执行基类的实例初始化器了,这时基类的成员第一次被调用,会出发基类的静态初始化器和静态构造函数,此后再执行基类的实例初始化器,并按顺序继续执行下去。

    因此最终的结果为:

    Derived static initializer
    Derived static constructor
    Derived initializer
    Base static initializer
    Base static constructor
    Base initializer
    Base constructor
    Derived constructor
    
  • 相关阅读:
    Citrix的一个安装问题(The Configuration file(s) for this site could not be read )
    MDOP(1) : AppV 命令行刷新
    BPC (9) SAP BI & BPC 安装 : 一个外行眼里的千奇百怪 (4)
    BPC (12) 服务账号密码重置(1)
    BPC (10) 二种平台的安装
    BPC (9) SAP BI & BPC 安装 : 一个外行眼里的千奇百怪 (3)
    BPC (11) – NW BPC 7.04 三个Bug
    一本30多年前的桥牌书
    使用XCode联机调试你的iOS应用
    $_SERVER 数据的一系列数据
  • 原文地址:https://www.cnblogs.com/kirinboy/p/csharp-puzzles-1-object-initialize-order.html
Copyright © 2011-2022 走看看