Effective C# 16.絕不在建構元中呼叫虛擬函式 Effective C# 16.絕不在建構元中呼叫虛擬函式 (Never Call Virtual Functions in Constructors)

Published on Saturday, October 12, 2024

這個做法在說明底層類別的建構函式直接呼叫虛擬方法會導致的問題,跟 LSP 原則提到的內容有點相似,overridenew 會改變抽象基底類別的行為。

以下面這段程式碼為例,乍看之下會以為結果會輸出 Set in main 但實際會輸出 Set by initializer

void Main()
{
	var d = new Derived("Set in main"); // 1.呼叫建構函式並傳入 Set in main
}

class B
{
	protected B() // 4. 運行基底建構函式並執行 VFunc 方法
	{
		VFunc(); 
	}
	protected virtual void VFunc()
	{
		Console.WriteLine("VFunc in B");
	}
}
class Derived : B
{
	private readonly string msg = "Set by initializer"; // 2. Member Initializers 優先執行
	public Derived(string msg) // 3. 準備執行建構函式但需要先執行基底建構函式
	{
		this.msg = msg; // 6. 將 Set by initializer 修改成 Set in main
	}
	protected override void VFunc() // 5. 因為基底建構函式呼叫所以直接輸出 Set by initializer
	{
		Console.WriteLine(msg);
	}
}

會有這樣的結果是因為在 Derived 類型初始化前需要先執行基底類型 B 的建構函式,因此 VFunc() 會優先執行,同時 override 已經覆蓋掉 基底的 VFunc() 方法,所以結果輸出為 Set by initializer

可以嘗試把 override 修改成 new 會看到輸出 VFunc in B 內容。

protected new void VFunc() // 5. 因為基底建構函式呼叫所以直接輸出 Set by initializer
{
	Console.WriteLine(msg);
}

並且在底層類別的建構函式直接呼叫虛擬方法會導致我們無法控制之後繼承類別的行為,也就是說我們之後自己寫的衍生類別都一定會執行 VFunc 方法, 因為這個行為已經在底層類別就已經決定好了。


Summary

在這個做法中建議不要在底層類別的建構函式直接呼叫虛擬方法,有兩大原因,第一個是程式碼執行的順序可能會與預測的不同,第二個是行為與底層類別 耦合導致我們不能自由控制衍生類別的行為。