1 引言
今天Artech兄在《關於Type Initializer和 BeforeFieldInit的問題,看看大家能否給出正確的解釋》一文中讓我們認識了一個關於類型構造器調用執行的有趣示例,其中也相應提出了一些關於beforefieldinit對於類型構造器調用時機的探討,對於我們很好的理解類型構造器給出了一個很好的應用實踐體驗。
作為補充,本文希望從基礎開始再層層深入,把《關於Type Initializer和 BeforeFieldInit的問題,看看大家能否給出正確的解釋》一文中沒有解釋的概念和原理,進行必要的補充,例如更全面的認識類型構造器,認識BeforeFieldInit。並在此基礎上,探討一點關於類型構造器的實踐應用,同時期望能夠回答其中示例運行的結果。
廢話少說,我們開始。
2 認識對象構造器和類型構造器
在.NET中,一個類的初始化過程是在構造器中進行的。並且根據構造成員的類型,分為類型構造器(.cctor)和對象構造器(.ctor), 其中.cctor和.ctor為二者在IL代碼中的指令表示。.cctor不能被直接調用,其調用規則正是本文欲加闡述的重點,詳見後文的分析;而.ctor會在類型實例化時被自動調用。
基於對類型構造器的探討,我們有必要首先實現一個簡單的類定義,其中包括普通的構造器和靜態構造器,例如
// Release : code01, 2008/11/02
// Author : Anytao, http://www.anytao.com
public class User
{
static User()
{
message = "Initialize in static constructor.";
}
public User()
{
message = "Initialize in normal construcotr.";
}
public User(string name, int age)
{
Name = name;
Age = age;
}
public string Name { get; set; }
public int Age { get; set; }
public static string message = "Initialize when defined.";
我們將上述代碼使用ILDasm.exe工具反編譯為IL代碼,可以很方便的找到相應的類型構造器和對象構造器的影子,如圖
然後,我們簡單的來了解一下對象構造器和類型構造器的概念。
對象構造器(.ctor)
在生成的IL代碼中將可以看到對應的ctor,類型實例化時會執行對應的構造器進行類型初始化的操作。
關於實例化的過程,設計到比較復雜的執行順序,按照類型基礎層次進行初始化的過程可以參閱《你必須知道的.NET》7.8節 “動靜之間:靜態和非靜態”一文中有詳細的介紹和分析,本文中將不做過多探討。
本文的重點以考察類型構造器為主,所以在此不進行過多探討。
類型構造器(.cctor)
用於執行對靜態成員的初始化,在.NET中,類型在兩種情況下會發生對.cctor的調用:
為靜態成員指定初始值,例如上例中只有靜態成員初始化,而沒有靜態構造函數時,.cctor的IL代碼實現為:
.method private hidebysig specialname rtspecialname static
void .cctor() cil managed
{
// Code size 11 (0xb)
.maxstack 8
IL_0000: ldstr "Initialize when defined."
IL_0005: stsfld string Anytao.Write.TypeInit.User::message
IL_000a: ret
} // end of method User::.cctor
實現顯式的靜態構造函數,例如上例中有靜態構造函數存在時,將首先執行靜態成員的初始化過程,再執行靜態構造函數初始化過程,.cctor的IL代碼實現為:
.method private hidebysig specialname rtspecialname static
void .cctor() cil managed
{
// Code size 23 (0x17)
.maxstack 8
IL_0000: ldstr "Initialize when defined."
IL_0005: stsfld string Anytao.Write.TypeInit.User::message
IL_000a: nop
IL_000b: ldstr "Initialize in static constructor."
IL_0010: stsfld string Anytao.Write.TypeInit.User::message
IL_0015: nop
IL_0016: ret
} // end of method User::.cctor
同時,我們必須明確一些靜態構造函數的基本規則,包括:
必須為靜態無參構造函數,並且一個類只能有一個。
只能對靜態成員進行初始化。
靜態無參構造函數可以和非靜態無參構造函數共存,區別在於二者的執行時間,詳見《你必須知道的.NET》7.8節 “動靜之間:靜態和非靜態”的論述,其他更多的區別和差異也詳見本節的描述。
3 深入執行過程
因為類型構造器本身的特點,在一定程度上決定了.cctor的調用時機並非是一個確定的概念。因為類型構造器都是private的,用戶不能顯式調用類型構造器。所以關於類型構造器的執行時機問題在.NET中主要包括兩種方案:
precise方式
beforefieldinit方式
二者的執行差別主要體現在是否為類型實現了顯式的靜態構造函數,如果實現了顯式的靜態構造函數,則按照precise方式執行;如果沒有實現顯式的靜態構造函數,則按照beforefieldinit方式執行。
為了說清楚類型構造器的執行情況,我們首先在概念上必須明確一個前提,那就是precise的語義明確了.cctor的調用和調用存取靜態成員的時機存在精確的關系,所以換句話說,類型構造器的執行時機在語義上決定於是否顯式的聲明了靜態構造函數,以及存取靜態成員的時機,這兩個因素。
我們還是從User類的實現說起,一一過招分析這兩種方式的執行過程。
3.1 precise方式
首先實現顯式的靜態構造函數方案,為:
// Release : code02, 2008/11/02
// Author : Anytao, http://www.anytao.com
public class User
{
//Explicit Constructor
static User()
{
message = "Initialize in static constructor.";
}
public static string message = "Initialize when defined.";
}
對應的IL代碼為:
.class public auto ansi User
extends [mscorlib]System.Object
{
.method private hidebysig specialname rtspecialname static void .cctor() cil managed
{
.maxstack 8
L_0000: ldstr "Initialize when defined."
L_0005: stsfld string Anytao.Write.TypeInit.User::message
L_000a: nop
L_000b: ldstr "Initialize in static constructor."
L_0010: stsfld string Anytao.Write.TypeInit.User::message
L_0015: nop
L_0016: ret
}
.method public hidebysig specialname rtspecialname instance void .ctor() cil managed
{
.maxstack 8
L_0000: ldarg.0
L_0001: call instance void [mscorlib]System.Object::.ctor()
L_0006: ret
}
.field public static string message
}
為了進行對比分析,我們需要首先分析beforefieldinit方式的執行情況,所以接著繼續。。。
3.2 beforefieldinit方式
為User類型,不實現顯式的靜態構造函數方案,為:
// Release : code03, 2008/11/02
// Author : Anytao, http://www.anytao.com
public class User
{
//Implicit Constructor
public static string message = "Initialize when defined.";
}
.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }
對應的IL代碼為:
.class public auto ansi beforefieldinit User
extends [mscorlib]System.Object
{
.method private hidebysig specialname rtspecialname static void .cctor() cil managed
{
.maxstack 8
L_0000: ldstr "Initialize when defined."
L_0005: stsfld string Anytao.Write.TypeInit.User::message
L_000a: ret
}
.method public hidebysig specialname rtspecialname instance void .ctor() cil managed
{
.maxstack 8
L_0000: ldarg.0
L_0001: call instance void [mscorlib]System.Object::.ctor()
L_0006: ret
}
.field public static string message
}
3.3 分析差別
從IL代碼的執行過程而言,我們首先可以了解的是在顯式和隱式實現類型構造函數的內部,除了添加新的初始化操作之外,二者的實現是基本相同的。所以要找出兩種方式的差別,我們最終將著眼點鎖定在二者元數據的聲明上,隱式方式多了一個稱為beforefieldinit標記的指令。
那麼,beforefieldinit究竟表示什麼樣的語義呢?Scott Allen對此進行了詳細的解釋:beforefieldinit為CLR提供了在任何時候執行.cctor的授權,只要該方法在第一次訪問類型的靜態字段之前執行即可。
所以,如果對precise方式和beforefieldinit方式進行比較時,二者的差別就在於是否在元數據聲明時標記了beforefieldinit指令。precise方式下,CLR必須在第一次訪問該類型的靜態成員或者實例成員之前執行類型構造器,也就是說必須剛好在存取靜態成員或者創建實例成員之前完成類型構造器的調用;beforefieldinit方式下,CLR可以在任何時候執行類型構造器,一定程度上實現了對執行性能的優化,因此較precise方式更加高效。
值得注意的是,當有多個beforefieldinit構造器存在時,CLR無法保證這多個構造器之間的執行順序,因此我們在實際的編碼時應該盡量避免這種情況的發生。
4 回歸問題,必要的小結
本文源於Artech兄的一個問題,希望通過上文的分析可以給出一點值得參考的背景。現在就關於Type Initializer和 BeforeFieldInit的問題,看看大家能否給出正確的解釋一文中的幾個示例進行一些繼續的分析:
在蔣兄的開始的示例實現中,可以很容易的來確定對於顯式實現了靜態構造函數的情況,類型構造器的調用在剛好引用靜態成員之前發生,所以不管是否在Main中聲明
string field = Foo.Field;
.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }
執行的結果不受影響。
而在沒有顯式實現靜態構造函數的情況下,beforefieldinit優化了類型構造器的執行不在確定的時間執行,只要實在靜態成員引用或者類型實例發生之前即可,所以在Debug環境下調用的時機變得不按常理。然而在Release優化模式下,beforefieldinit的執行順序並不受
string field = Foo.Field;
.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }
的影響,完全符合beforefieldinit優化執行的語義定義。
關於最後一個靜態成員繼承情況的結果,正像本文開始描述的邏輯一樣,類型構造器是在靜態成員被調用或者創建實例時發生,所以示例的結果是完全遵守規范的。不過,我並不建議子類最好不要調用父類靜態成員,原因是作為繼承機制而言,子承父業是繼承的基本規范,除了強制為private之外,所有的成員或者方法都應在子類中可見。而對於存在的潛在問題,更好的以規范來約束可能會更好。其中,靜態方法一定程度上是一種結構化的實現機制,在面向對象的繼承關系中,本質上就存在一定的不足。
在c#規范中,關於beforefieldinit的控制已經引起很多的關注和非議,一方面beforefieldinit方式可以有效的優化調用性能,但是以顯式和或者隱式實現靜態構造函數的方式不能更有直觀的讓程序開發者來控制,因此在以後版本的c#中,能實現基於特性的聲明方式來控制,是值得期待的。
另一方面,在有兩個類型的類型構造器相互引用的情況下,CLR無法保證類型構造器的調用順序,對程序開發者而言,我同樣強調了對於類型構造器而言,我們應該盡量避免要求順序相關的業務邏輯,因為很多時候執行的順序並非聲明的順序,這是值得關注的。
5 結論
除了補充Artech老兄的問題,本文算是繼續了關於類型構造器在《你必須知道的.NET》7.8節 “動靜之間:靜態和非靜態”中的探討,以更全面的視角來進一步闡釋這個問題。在最後,關於beforefieldinit標記引起的類型構造器調用優化的問題,雖然沒有完全100%的了解在Debug模式下的CLR調用行為,但是深入細節我們可以掌控對於語言之內更多的理解,從這點而言,本文是個開始。