謎題
在上一篇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