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