[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
的构造函数中调用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
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步