[你必須知道的.NET]第二十三回:品味細節,深入.NET的類型構造器
[你必須知道的.NET]第二十三回:品味細節,深入.NET的類型構造器
發布日期:2008.11.2 作者:Anytao
? 2008 Anytao.com ,Anytao原創作品,轉貼請注明作者和出處。
| 今天Artech兄在《關于Type Initializer和 BeforeFieldInit的問題,看看大家能否給出正確的解釋》一文中讓我們認識了一個關于類型構造器調用執行的有趣示例,其中也相應提出了一些關于beforefieldinit對于類型構造器調用時機的探討,對于我們很好的理解類型構造器給出了一個很好的應用實踐體驗。 認識類型構造器,認識beforefieldinit,更深入關注CLR執行機理,品味細節之美。 |
今天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)
關于實例化的過程,設計到比較復雜的執行順序,按照類型基礎層次進行初始化的過程可以參閱《你必須知道的.NET》7.8節 “動靜之間:靜態和非靜態”一文中有詳細的介紹和分析,本文中將不做過多探討。
本文的重點以考察類型構造器為主,所以在此不進行過多探討。
- 類型構造器(.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.";
}
對應的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兄的一個問題,希望通過上文的分析可以給出一點值得參考的背景?,F在就關于Type Initializer和 BeforeFieldInit的問題,看看大家能否給出正確的解釋一文中的幾個示例進行一些繼續的分析:
- 在蔣兄的開始的示例實現中,可以很容易的來確定對于顯式實現了靜態構造函數的情況,類型構造器的調用在剛好引用靜態成員之前發生,所以不管是否在Main中聲明
string field = Foo.Field;
執行的結果不受影響。
- 而在沒有顯式實現靜態構造函數的情況下,beforefieldinit優化了類型構造器的執行不在確定的時間執行,只要實在靜態成員引用或者類型實例發生之前即可,所以在Debug環境下調用的時機變得不按常理。然而在Release優化模式下,beforefieldinit的執行順序并不受
string field = Foo.Field;
的影響,完全符合beforefieldinit優化執行的語義定義。
- 關于最后一個靜態成員繼承情況的結果,正像本文開始描述的邏輯一樣,類型構造器是在靜態成員被調用或者創建實例時發生,所以示例的結果是完全遵守規范的。不過,我并不建議子類最好不要調用父類靜態成員,原因是作為繼承機制而言,子承父業是繼承的基本規范,除了強制為private之外,所有的成員或者方法都應在子類中可見。而對于存在的潛在問題,更好的以規范來約束可能會更好。其中,靜態方法一定程度上是一種結構化的實現機制,在面向對象的繼承關系中,本質上就存在一定的不足。
- 在c#規范中,關于beforefieldinit的控制已經引起很多的關注和非議,一方面beforefieldinit方式可以有效的優化調用性能,但是以顯式和或者隱式實現靜態構造函數的方式不能更有直觀的讓程序開發者來控制,因此在以后版本的c#中,能實現基于特性的聲明方式來控制,是值得期待的。
- 另一方面,在有兩個類型的類型構造器相互引用的情況下,CLR無法保證類型構造器的調用順序,對程序開發者而言,我同樣強調了對于類型構造器而言,我們應該盡量避免要求順序相關的業務邏輯,因為很多時候執行的順序并非聲明的順序,這是值得關注的。
5 結論
除了補充Artech老兄的問題,本文算是繼續了關于類型構造器在《你必須知道的.NET》7.8節 “動靜之間:靜態和非靜態”中的探討,以更全面的視角來進一步闡釋這個問題。在最后,關于beforefieldinit標記引起的類型構造器調用優化的問題,雖然沒有完全100%的了解在Debug模式下的CLR調用行為,但是深入細節我們可以掌控對于語言之內更多的理解,從這點而言,本文是個開始。
2008/11/02 | 榮譽出品:http://www.rzrgm.cn/anytao
本文以“現狀”提供且沒有任何擔保,同時也沒有授予任何權利。 | This posting is provided "AS IS" with no warranties, and confers no rights.
本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。
參考文獻
溫故知新
[開篇有益]
[第一回:恩怨情仇:is和as]
[第二回:對抽象編程:接口和抽象類]
[第三回:歷史糾葛:特性和屬性]
[第四回:后來居上:class和struct]
[第五回:深入淺出關鍵字---把new說透]
[第六回:深入淺出關鍵字---base和this]
[第七回:品味類型---從通用類型系統開始]
[第八回:品味類型---值類型與引用類型(上)-內存有理]
[第九回:品味類型---值類型與引用類型(中)-規則無邊]
[第十回:品味類型---值類型與引用類型(下)-應用征途]
[第十一回:參數之惑---傳遞的藝術(上)]
[第十二回:參數之惑---傳遞的藝術(下)]
[第十三回:從Hello, world開始認識IL]
[第十四回:認識IL代碼---從開始到現在]
[第十五回:繼承本質論]
[第十六回:深入淺出關鍵字---using全接觸]
[第十七回:貌合神離:覆寫和重載]
[第十八回:對象創建始末(上)]
[第十九回:對象創建始末(下)]
[第二十回:學習方法論]
[第二十一回:認識全面的null]
[第二十二回:字符串駐留(上)---帶著問題思考]
Worktile,新一代簡單好用、體驗極致的團隊協同、項目管理工具,讓你和你的團隊隨時隨地一起工作。完全免費,現在就去了解一下吧。
https://worktile.com

今天Artech兄在《關于Type Initializer和 BeforeFieldInit的問題,看看大家能否給出正確的解釋》一文中讓我們認識了一個關于類型構造器調用執行的有趣示例,其中也相應提出了一些關于beforefieldinit對于類型構造器調用時機的探討,對于我們很好的理解類型構造器給出了一個很好的應用實踐體驗。
認識類型構造器,認識beforefieldinit,更深入關注CLR執行機理,品
浙公網安備 33010602011771號