[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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM