[你必须知道的.NET]第二十三回:品味细节,深入.NET的类型构造器
今天Artech兄在《關于Type Initializer和 BeforeFieldInit的問題,看看大家能否給出正確的解釋》一文中讓我們認識了一個關于類型構造器調用執行的有趣示例,其中也相應提出了一些關于beforefieldinit對于類型構造器調用時機的探討,對于我們很好的理解類型構造器給出了一個很好的應用實踐體驗。?
作為補充,本文希望從基礎開始再層層深入,把《關于Type Initializer和 BeforeFieldInit的問題,看看大家能否給出正確的解釋》一文中沒有解釋的概念和原理,進行必要的補充,例如更全面的認識類型構造器,認識BeforeFieldInit。并在此基礎上,探討一點關于類型構造器的實踐應用,同時期望能夠回答其中示例運行的結果。?
廢話少說,我們開始。
2 認識對象構造器和類型構造器
在.NET中,一個類的初始化過程是在構造器中進行的。并且根據構造成員的類型,分為類型構造器(.cctor)和對象構造器(.ctor), 其中.cctor和.ctor為二者在IL代碼中的指令表示。.cctor不能被直接調用,其調用規則正是本文欲加闡述的重點,詳見后文的分析;而.ctor會在類型實例化時被自動調用。?
基于對類型構造器的探討,我們有必要首先實現一個簡單的類定義,其中包括普通的構造器和靜態構造器,例如
我們將上述代碼使用ILDasm.exe工具反編譯為IL代碼,可以很方便的找到相應的類型構造器和對象構造器的影子,如圖
然后,我們簡單的來了解一下對象構造器和類型構造器的概念。
- 對象構造器(.ctor)
在生成的IL代碼中將可以看到對應的ctor,類型實例化時會執行對應的構造器進行類型初始化的操作。?
關于實例化的過程,設計到比較復雜的執行順序,按照類型基礎層次進行初始化的過程可以參閱《你必須知道的.NET》7.8節 “動靜之間:靜態和非靜態”一文中有詳細的介紹和分析,本文中將不做過多探討。?
本文的重點以考察類型構造器為主,所以在此不進行過多探討。
- 類型構造器(.cctor)
用于執行對靜態成員的初始化,在.NET中,類型在兩種情況下會發生對.cctor的調用:
- 為靜態成員指定初始值,例如上例中只有靜態成員初始化,而沒有靜態構造函數時,.cctor的IL代碼實現為:
- 實現顯式的靜態構造函數,例如上例中有靜態構造函數存在時,將首先執行靜態成員的初始化過程,再執行靜態構造函數初始化過程,.cctor的IL代碼實現為:
同時,我們必須明確一些靜態構造函數的基本規則,包括:
- 必須為靜態無參構造函數,并且一個類只能有一個。
- 只能對靜態成員進行初始化。
- 靜態無參構造函數可以和非靜態無參構造函數共存,區別在于二者的執行時間,詳見《你必須知道的.NET》7.8節 “動靜之間:靜態和非靜態”的論述,其他更多的區別和差異也詳見本節的描述。
3 深入執行過程
因為類型構造器本身的特點,在一定程度上決定了.cctor的調用時機并非是一個確定的概念。因為類型構造器都是private的,用戶不能顯式調用類型構造器。所以關于類型構造器的執行時機問題在.NET中主要包括兩種方案:
- precise方式
- beforefieldinit方式
二者的執行差別主要體現在是否為類型實現了顯式的靜態構造函數,如果實現了顯式的靜態構造函數,則按照precise方式執行;如果沒有實現顯式的靜態構造函數,則按照beforefieldinit方式執行。?
為了說清楚類型構造器的執行情況,我們首先在概念上必須明確一個前提,那就是precise的語義明確了.cctor的調用和調用存取靜態成員的時機存在精確的關系,所以換句話說,類型構造器的執行時機在語義上決定于是否顯式的聲明了靜態構造函數,以及存取靜態成員的時機,這兩個因素。?
我們還是從User類的實現說起,一一過招分析這兩種方式的執行過程。?
3.1 precise方式?
首先實現顯式的靜態構造函數方案,為:
對應的IL代碼為:
<span style="color:black"><span style="color:black">.<span style="color:#0000ff">class</span> <span style="color:#0000ff">public</span> auto ansi User</span></span> extends [mscorlib]System.Object <span style="color:black"><span style="color:black">{</span></span> .method private hidebysig specialname rtspecialname static void .cctor() cil managed <span style="color:black"><span style="color:black"> {</span></span> .maxstack 8 <span style="color:black"><span style="color:black"> L_0000: ldstr <span style="color:#006080">"Initialize when defined."</span></span></span> L_0005: stsfld string Anytao.Write.TypeInit.User::message <span style="color:black"><span style="color:black"> L_000a: nop </span></span> L_000b: ldstr "Initialize in static constructor." <span style="color:black"><span style="color:black"> L_0010: stsfld <span style="color:#0000ff">string</span> Anytao.Write.TypeInit.User::message</span></span> L_0015: nop <span style="color:black"><span style="color:black"> L_0016: ret </span></span> } <span style="color:black"><span style="color:black">?</span></span> .method public hidebysig specialname rtspecialname instance void .ctor() cil managed <span style="color:black"><span style="color:black"> {</span></span> .maxstack 8 <span style="color:black"><span style="color:black"> L_0000: ldarg.0 </span></span> L_0001: call instance void [mscorlib]System.Object::.ctor() <span style="color:black"><span style="color:black"> L_0006: ret </span></span> } <span style="color:black"><span style="color:black">?</span></span> .field public static string message <span style="color:black"><span style="color:black">}</span></span>為了進行對比分析,我們需要首先分析beforefieldinit方式的執行情況,所以接著繼續。。。?
3.2 beforefieldinit方式?
為User類型,不實現顯式的靜態構造函數方案,為:
對應的IL代碼為:
<span style="color:black"><span style="color:black">.<span style="color:#0000ff">class</span> <span style="color:#0000ff">public</span> auto ansi beforefieldinit User</span></span> extends [mscorlib]System.Object <span style="color:black"><span style="color:black">{</span></span> .method private hidebysig specialname rtspecialname static void .cctor() cil managed <span style="color:black"><span style="color:black"> {</span></span> .maxstack 8 <span style="color:black"><span style="color:black"> L_0000: ldstr <span style="color:#006080">"Initialize when defined."</span></span></span> L_0005: stsfld string Anytao.Write.TypeInit.User::message <span style="color:black"><span style="color:black"> L_000a: ret </span></span> } <span style="color:black"><span style="color:black">?</span></span> .method public hidebysig specialname rtspecialname instance void .ctor() cil managed <span style="color:black"><span style="color:black"> {</span></span> .maxstack 8 <span style="color:black"><span style="color:black"> L_0000: ldarg.0 </span></span> L_0001: call instance void [mscorlib]System.Object::.ctor() <span style="color:black"><span style="color:black"> L_0006: ret </span></span> } <span style="color:black"><span style="color:black">?</span></span> .field public static string message <span style="color:black"><span style="color:black">}</span></span>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中聲明
執行的結果不受影響。
- 而在沒有顯式實現靜態構造函數的情況下,beforefieldinit優化了類型構造器的執行不在確定的時間執行,只要實在靜態成員引用或者類型實例發生之前即可,所以在Debug環境下調用的時機變得不按常理。然而在Release優化模式下,beforefieldinit的執行順序并不受
的影響,完全符合beforefieldinit優化執行的語義定義。
- 關于最后一個靜態成員繼承情況的結果,正像本文開始描述的邏輯一樣,類型構造器是在靜態成員被調用或者創建實例時發生,所以示例的結果是完全遵守規范的。不過,我并不建議子類最好不要調用父類靜態成員,原因是作為繼承機制而言,子承父業是繼承的基本規范,除了強制為private之外,所有的成員或者方法都應在子類中可見。而對于存在的潛在問題,更好的以規范來約束可能會更好。其中,靜態方法一定程度上是一種結構化的實現機制,在面向對象的繼承關系中,本質上就存在一定的不足。
- 在c#規范中,關于beforefieldinit的控制已經引起很多的關注和非議,一方面beforefieldinit方式可以有效的優化調用性能,但是以顯式和或者隱式實現靜態構造函數的方式不能更有直觀的讓程序開發者來控制,因此在以后版本的c#中,能實現基于特性的聲明方式來控制,是值得期待的。
- 另一方面,在有兩個類型的類型構造器相互引用的情況下,CLR無法保證類型構造器的調用順序,對程序開發者而言,我同樣強調了對于類型構造器而言,我們應該盡量避免要求順序相關的業務邏輯,因為很多時候執行的順序并非聲明的順序,這是值得關注的。
5 結論
除了補充Artech老兄的問題,本文算是繼續了關于類型構造器在《你必須知道的.NET》7.8節 “動靜之間:靜態和非靜態”中的探討,以更全面的視角來進一步闡釋這個問題。在最后,關于beforefieldinit標記引起的類型構造器調用優化的問題,雖然沒有完全100%的了解在Debug模式下的CLR調用行為,但是深入細節我們可以掌控對于語言之內更多的理解,從這點而言,本文是個開始。
?
支持anytao的創業產品WorktileWorktile,新一代簡單好用、體驗極致的團隊協同、項目管理工具,讓你和你的團隊隨時隨地一起工作。完全免費,現在就去了解一下吧。
https://worktile.com
?
參考文獻
- 《你必須知道的.NET》7.8節 “動靜之間:靜態和非靜態”
- Artech,關于Type Initializer和 BeforeFieldInit的問題,看看大家能否給出正確的解釋
- 通過七個關鍵編程技巧得益于靜態內容
-
#53樓?2009-10-27 22:25?fisea
請教lz一個問題:
如下程序代碼:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class?Program?????
???{?????????
???????static?void?Main()????????
???????{
???????????Console.WriteLine("Start ...");
???????????Foo.GetString("Manually invoke the static GetString() method!");
???????}?????
???}
???class?Foo
???{
???????public?static?string?Field = GetString("Initialize the static field!");
???????public?static?string?GetString(string?s)
???????{
???????????Console.WriteLine(s);
???????????return?s;
???????}
???}
的運行結果如下:
Start ...
Initialize the static field!
Manually invoke the static GetString() method!
和如下代碼:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class?Program?????
???{?????????
???????static?void?Main()????????
???????{
???????????Console.WriteLine("Main execute!");
???????????Console.WriteLine("int: "?+ MyClass<int>.Time);
???????????Thread.Sleep(3000);
???????????Console.WriteLine("string: "?+ MyClass<string>.Time);
???????????Console.ReadLine();
???????}?????
???}
???public?static?class?MyClass<T>
???{
???????public?static?readonly?DateTime Time = GetNow();
???????private?static?DateTime GetNow()
???????{
???????????Console.WriteLine("GetNow execute!");
???????????return?DateTime.Now;
???????}
???}
的運行結果:
GetNow execute!
GetNow execute!
Main execute!
int: 2009-10-27 22:20:06
string: 2009-10-27 22:20:06
問題:
1、第一個例子為什么先執行Main中的Console.WriteLine("Start ...");
而第二個例子是先執行MyClass類中GetNow()函數。
2、第二個例子中的運行結果中為什么有兩個GetNow execute!
GetNow execute!。請教lz。謝謝。支持(0)?反對(0)
??
#54樓?2010-05-05 22:12?Edenia
LZ能否回答一下53樓的問題,我也感覺很迷茫,O(∩_∩)O謝謝了~
支持(0)?反對(0)
??
#55樓?2010-11-29 21:05?李董
引用fisea:請教lz一個問題:
如下程序代碼:
[code=csharp]
class Program?
{?
static void Main()?
{
Console.WriteLine("Start ...");
Foo.GetString("Manually invoke the static GetString() method!");
}?
}
class Foo
{
public ...
第一段代碼中運行到Foo.GetString("Manually invoke the static GetString() method!");這句時,會先執行該類的靜態變量和靜態構造函數(此處未顯示定義靜態構造函數),靜態變量初始化后再執行上面的這句方法。支持(0)?反對(0)
??
#56樓?2011-02-16 16:14?王磊的博客
晚上睡覺不 哥,不累啊,自娛自樂的瘋狂技術俠客,佩服!
支持(0)?反對(0)
??
#57樓?2013-07-27 11:50?String.Trim()
@?Edenia
@fisea
1.第二段代碼中有
Console.WriteLine("int: " + MyClass<int>.Time);
Console.WriteLine("string: " + MyClass<string>.Time);
對Time靜態成員進行引用,
而MyClass<T>類中沒有類型(靜態)構造器,所以CLR可以在任何時候執行類型構造器,只要是在靜態成員引用或者類型實例發生之前(不包括靜態方法)即可。會在Main方法之前執行。
第一段代碼沒有對靜態成員進行引用,所以會按照順序執行代碼。
2.泛型類只有在具有相同類型形參的類型的實例才能夠共享同一個靜態字段的值。MyClass<int>與MyClass<string>中的靜態字段值是不同的,所以GetNow()會執行2遍。
總結
以上是生活随笔為你收集整理的[你必须知道的.NET]第二十三回:品味细节,深入.NET的类型构造器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 今年最大召回!变速箱“翻车” 福特召回近
- 下一篇: NAS迅雷正式上架威联通:支持磁力链、B