承接上篇,本文揭示實際執行順序:派生類字段初始化 → 基類字段初始化 → 基類構造函數 → 派生類構造函數,並通過反例說明為何必須如此設計。
BaseConstructor()
{
baseFoo = new Foo("Base initializer");
ObjectConstructor();
Console.WriteLine("Base constructor");
}
DerivedConstructor()
{
derivedFoo = new Foo("Derived initializer");
BaseConstructor();
Console.WriteLine("Derived constructor");
}
假設按「期望順序」(基類構造函數先於派生類字段初始化)執行,會導致嚴重問題:
class Base
{
public Base()
{
if (this is Derived) ((Derived)this).DoIt(); // 派生類字段尚未初始化 → NullReference
Blah(); // 若被 MoreDerived 重寫並調用 DoIt,同樣崩潰
}
public virtual void Blah() { }
}
class Derived : Base
{
readonly Foo derivedFoo = new Foo("Derived initializer");
public void DoIt() { derivedFoo.Bar(); }
}
class MoreDerived : Derived
{
public override void Blah() { DoIt(); }
}
若基類構造函數先執行,派生類的 readonly 字段 derivedFoo 仍為 null,調用 DoIt 即拋出異常。實際順序保證所有實例字段在任何構造函數體執行前已完成初始化,從而避免此類缺陷。
若 readonly 字段在構造函數中賦值而非初始化器,則上述保護機制失效,需自行確保調用順序安全。