1、靜態構造函數
在引入本文的主題之前,我們先來鋪墊一下吧,看看靜態構造函數的概念及用途。
C#中允許創建無參數構造函數,該函數僅執行一次。它一般被用來初始化靜態字段。CLR不能保證在某個特定時刻執行靜態構造函數,同時也不保證不同類的靜態構造函數按照什么順序執行,但保證它僅執行一次,即在應用程序創建該類的第一個實例或訪問該類的任何靜態成員之前。
注意,靜態構造函數不允許有訪問修飾符,且不接受任何參數,這是因為其他代碼沒有權利調用它,它的調用執行總是被CLR接管的!另外,一個類只能有一個靜態構造函數。
2、字段及構造函數的初始化順序
首先,我們需要明確的一點是:字段和構造函數均分為靜態和實例兩大類。其中,靜態的屬於類型本身,僅有一份;而實例的屬於創建的實例對象,可有多份。靜態的總是先於實例的被初始化。
在初始化一個C#對象時,如果我們曉得字段和構造函數被初始化的順序,就能夠防止一些錯誤發生,比如引用還未初始化的字段等,同時對對象初始化構造的過程有更深刻的認識。
首先,我們先給出初始化先后順序的結論,然后再以例子來佐證:
1、繼承類靜態字段
2、繼承類靜態構造函數
3、繼承類實例字段
4、基類靜態字段
5、基類靜態構造函數
6、基類實例字段
7、基類實例構造函數
8、繼承類實例構造函數
我們知道對於繼承層次結構的類來說,首先調用基類的構造函數,然后調用繼承類的構造函數。
下面我們以一個控制台程序的執行來說明。
class Program { static void Main(string[] args) { Derived d = new Derived(); Console.ReadLine(); } } class Base { public Base() { Console.WriteLine("基類實例構造器"); this.m_Field3 = new ShowMessage("基類實例字段3"); this.Virtual(); } static Base() { Console.WriteLine("基類靜態構造器"); } private ShowMessage m_Field1 = new ShowMessage("基類實例字段1"); private ShowMessage m_Field2 = new ShowMessage("基類實例字段2"); private ShowMessage m_Field3; static private ShowMessage s_Field1 = new ShowMessage("基類靜態字段1"); static private ShowMessage s_Field2 = new ShowMessage("基類靜態字段2"); virtual public void Virtual() { Console.WriteLine("基類實例虛方法"); } } class Derived : Base { public Derived() { Console.WriteLine("繼承類實例構造器"); this.m_Field3 = new ShowMessage("繼承類實例字段3"); } static Derived() { Console.WriteLine("繼承類靜態構造器"); } private ShowMessage m_Field1 = new ShowMessage("繼承類實例字段1"); private ShowMessage m_Field2 = new ShowMessage("繼承類實例字段2"); private ShowMessage m_Field3; static private ShowMessage s_Field1 = new ShowMessage("繼承類靜態字段1"); static private ShowMessage s_Field2 = new ShowMessage("繼承類靜態字段2"); override public void Virtual() { Console.WriteLine("繼承類實例虛方法"); } } class ShowMessage { public ShowMessage(string msg) { Console.WriteLine(msg); } } }
上面的程序中,Drived子類和Base基類均包含靜態和實例構造器以及靜態和實例字段。其中,實例字段m_Field1和m_Field2在字段定義時初始化,而字段m_Field3在實例構造起中初始化。同時,在Base基類的構造器中調用了一個Virtual虛方法,這一步是為了說明在構造器中調用虛方法存在的潛在風險,實際開發中應避免這樣做。
從上面的執行過程中,可以看出,繼承類靜態字段和靜態構造器首先初始化,很明顯,靜態的東西是類型本身固有的東西,所以,在初始化一個實例對象之前,首先要保證靜態的被初始化。而無論靜態還是實例字段,它們的初始化過程總是優先於相對應的構造函數中的其余代碼的初始化。
在繼承類的靜態字段和構造器執行完畢之后,接着執行實例字段1和2的初始化,這兩個字段屬於在定義時初始化,先於實例構造函數的調用,這是因為要防止在構造函數中調用未初始化的字段(構造器若調用了一些方法,而這些方法訪問了未初始化的字段就會發生這種情況)。我們之所以將字段的初始化提前到構造函數執行之前,目的就在於避免null reference exceptions,這在大多數情況下是更好的選擇,但這種方式也有缺點,即:在調試對象初始化部分代碼時,若想進入構造函數必須先經過一系列的字段成員初始化代碼,尤其在繼承層次復雜時,這種混亂更加明顯。在這兩個字段初始化之后,開始實例構造函數的調用,由於存在繼承關系,故先調用基類的構造函數。
在調用基類的構造函數之前,首先要初始化基類的靜態字段和靜態構造器,原因同繼承類。然后初始化基類的實例字段1和2,接着調用基類實例構造器並初始化實例字段3。接着在基類的實例構造器中調用了Virtual虛方法。這里需要注意的是:這時候繼承類的實例構造函數還沒有被執行,如果此虛方法中需要使用已初始化的字段,比如本例中的字段3,那么,程序將會導致錯誤。這提示我們在編程時應盡量避免在構造函數中調用虛方法,而應在對象初始化完成之后再調用虛方法。
3、字段初始化器和構造函數初始化字段的差異
兩者的差異主要有兩方面:
1、用字段初始化器初始化字段就不能使用this,因為此時構造函數還未調用。
2、第二個差異點就在於存在繼承層次結構時,因為在這種情形下字段初始化器和構造函數的執行順序不一致,前者的執行順序是由繼承類到基類,后者是由基類到繼承類。
最后,有一些情形需要特別注意,比如下面的例子,請讀者自行思考。
class Base { private readonly object objectA = new object(); // 第二執行 private readonly object objectB; public Base() { this.objectB = new object(); // 第三執行 } } class Derived : Base { private object objectC = new object(); // 首先執行 private object objectD; public Derived() { this.objectD = new object(); // 第四執行 } }