禁止在構造函數裏調用虛函數

在構造函數中調用虛函數會導致程序出現莫名其妙的行爲,這主要是對象還沒有完全構造完成。下面我們先來看一段代碼:

class B
{
    protected B()
    {
        Method();
    }
    protected virtual void Method()
    {
        Console.WriteLine("B Method");
    }
}
class A:B
{
    private readonly string str = "你好";
    public A(string str)
    {
        this.str=str;
    }
    protected override void Method()
    {
        Console.WriteLine(str);
    }
    public static void Main()
    {
        var d = new A("A Method");
    }
}

在這裏我要問一下各位讀者,在上述代碼中打印出的內容是什麼?大部分讀者會回答 “A Method” ,實際上的答案是 “你好” 。這是爲什麼呢?這是因爲基類的構造函數調用一個定義在本類中的但是爲派生類所重寫的虛函數,程序運行的時候會調用派生類的版本,程序在運行期的類型是 A 而不是 B。在 C# 中系統會認爲這個對象是一個可以正常使用的對象,這是因爲程序在進入構造函數的函數體之前已經把該對象的所有成員變量都進行了初始化。但是者並不意味着這些成員變量的值和開發人員最終想要的值相符,因爲程序僅僅執行了成員變量的初始化語句,而沒有執行構造函數中的邏輯。
我們將前面的代碼稍加修改看一下:

abstract class B
{
    protected B()
    {
        Method();
    }
    protected abstract void Method();
}
class A:B
{
    private readonly string str = "你好";
    public A(string str)
    {
        this.str=str;
    }
    protected override void Method()
    {
        Console.WriteLine(str);
    }
    public static void Main()
    {
        var d = new A("A Method");
    }
}

針對上述代碼,我想問問各位讀者,這段代碼可以通過編譯碼?答案是可以通過編譯,這是因爲程序就不會創建一個類型爲 B 的對象,他創建的對象只是 B 的實現了 Method 方法的子類,程序代碼所運行的也是那個子類的 Method 方法。這麼做主要是爲了避免在構造函數中調用抽象類中的方法,防止拋出異常。雖然這麼寫可以避免這個問題但是還存在一個很大的缺陷,它會造成 str 這個對象在整個生命週期中無法保持恆定的值。在構造函數還沒有把該對象初始化完成之前,它的取值是由初始化語句決定的,但是執行完構造函數之後它的值卻變成了構造函數中所設定的那個值。派生類對象所具備的成員變量的默認值是由初始化語句或者系統來確定的,因此開發人員如果想要在構造函數中給這些變量賦值那麼就必須等到程序運行到構造函數時纔可以。

Tip:C# 對象的運行期類型是一開始就定好的,即便基類是抽象類也依然可以調用其中的虛方法。

小結

在基類構造函數中調用虛函數會導致代碼嚴重依賴於派生類的實現,然後這些實現是無法控制且容易出錯的。如果要避免錯誤,派生類就必須通過初始化語句把所有的實例變量設置好,但是這又會使得開發人員無法運用更多的編程技巧。也就是說在這種情況下派生類必須定義默認構造函數,並且不能定義別的構造函數,這將會給開發人員帶來很大的負擔。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章