Chapter 2 - Item 16 : Never Call Virtual Functions in Constructors

Effective C# (Covers C# 6.0), (includes Content Update Program): 50 Specific Ways to Improve Your C#, 3rd Edition By Bill Wagner 讀後心得

虛方法若放置在基類的建構子中,會發生讓人無法預料的結果(視子類別覆寫的內容而定)。以日後維護的觀點來看,應避免這樣的設計。接著,比較 C++ 與 C# 對虛方法或抽象方法的處理方式有何不同。

考慮以下程式碼:

class B
{
    protected B( )
    {
        vFunc( );
    }
    
    protected virtual void vFunc( )
    {
        Debug.WriteLine( "vFunc in B" );
    }
}

class Derived : B
{
    private readonly string _msg = "Set by initializer";
    
    public Derived( string msg )
    {
        _msg = msg;
    }
    
    protected override void vFunc( )
    {
        Debug.WriteLine( _msg );
    }
}

Client:

var d = new Derived( "Constructed by Client" );

輸出:
Set by initializer

依照 item 15 的結論 2. 物件建構順序以及 C# 對 virtual function 的定義(i.e. 由 runtime type 決定該呼叫哪個類別的實作,在此例子中 type 為 Derived)。但即便編譯與運行是合法的,也並非良好的設計;_msg 是由 Derived 的建構子決定,但此例子輸出卻是類別成員初始化時的字串。顯然這樣會造成混淆,設計原意是要輸出哪一個字串呢?

Note:C++ 在此例子中會輸出 vFunc in B,原因在於 C++ 會依照當下執行類別的建構子改變其 runtime type。

接下來如果我們把 B 與 vfunc 定義為 abstract 會如何?

abstract class B
{
    protected B( )
    {
        vFunc( );
    }
    
    protected abstract void vFunc( );
}

class Derived : B
{
    private readonly string _msg = "Set by initializer";
    
    public Derived( string msg )
    {
        _msg = msg;
    }
    
    protected override void vFunc( )
    {
        Debug.WriteLine( _msg );
    }
}

Client:

var d = new Derived( "Constructed by Client" );

• C# 仍然會輸出 Set by initializer。
• C++ 則會 crash。

結論:
1. 應避免在基類呼叫虛方法或抽象方法。

2. 了解 C++ 與 C# 在處理虛方法與抽象方法相異之處。