[C#解惑] #2 对象的初始化顺序
2016-02-29 00:02
417 查看
谜题
在上一篇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的构造函数中调用
Derived的
N时,
derivedFoo就会为
null,因为它还没有初始化。试想一下,你正在调用一个对象的方法,但这个对象的字段没有初始化,构造函数也还没有执行,这显然是不合理的。
同样,在构造
MoreDerived时,在
Base的构造函数中调用
M(进而调用
N)也会得到空引用,因为
Derived的
derivedFoo仍然没有初始化。
注意 尽管类似
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