Internal sealed class SomeType { private Int32 m_x = 5;} 我們來看下生成的IL代碼吧,著重看下.ctor方法中的代碼!
明顯可以看出,實例構造器把值5存到字段m_x,再調用基類的構造器。也就是說,C#編譯器提供了一種簡化的語法,允許以"內聯"的方式初始化實例字段。編譯器幫助我們調用構造器方法來執行初始化,但是我們要注意代碼的"膨脹效應",如下類定義:internal sealed class SomeType { private Int32 m_x = 5; private String m_s = "Hi there"; private Double m_d = 3.14159; private Byte m_b; // Here are some constructors. public SomeType() { /* ... */ } public SomeType(Int32 x) { /* ... */ } public SomeType(String s) { /* ...; */ m_d = 10; } } 使用反編譯器會查看到有三個構造器.ctor方法。 
我們在看下每個構造器方法的IL代碼。 .ctor() 
.ctor(Int32) 
.ctor(String) 
有何發現呢? 編譯器為這三個構造器方法生成代碼時,在每個方法的開始位置,都會包含用于初始化m_x,m_s和m_d的代碼。在這些初始化代碼之后,編譯器會插入對基類構造器的調用。再然后,會插入構造器自己的代碼。 上面類中有三個構造器,所以編譯器就生成的三次初始化的m_x,m_s和m_d的代碼,由于兩方面都會增多,所有就會造成"代碼膨脹"的問題。 那如何解決呢? 可以考慮不是在定義字段的時候初始化,而是創建單個構造器來執行這些公共的初始化。然后,讓其他構造器顯示調用這個公共初始化構造器。internal sealed class SomeType { // 不顯示初始化下面的字段 private Int32 m_x; private String m_s; private Double m_d; private Byte m_b; // 該構造器將所有的字段都設為默認值 // 其它所有構造器顯式調用這個構造器 public SomeType() { m_x = 5; m_s = "Hi there"; m_d = 3.14159; m_b = 0xff; } // 該構造器將所有的字段都設為默認值,然后修改m_x public SomeType(Int32 x) : this() { m_x = x; } // 該構造器將所有的字段都設為默認值,然后修改m_x public SomeType(String s) : this() { m_s = s; } // 該構造器將所有的字段都設為默認值,然后修改m_x和m_s public SomeType(Int32 x, String s) : this() { m_x = x; m_s = s; } }二、實例構造器和結構(值類型)
值類型(struct)構造器的工作方式與引用類型(class)的構造器截然不同。CLR總是允許創建值類型的實例,并且沒有辦法阻止值類型的實例化。所以,值類型其實并不需要定義構造器,C#編譯器根本不會為值類型生成默認的無參構造器。如以下代碼:
internal struct Point{ pblic Int32 m_x, m_y;}internal sealed class Rectangle { public Point m_topLeft, m_bottomRight;} 為了構造一個類Rectangle,必須使用new操作符,而且必須指定一個構造器。在這個例子中,調用的是C#自動生成的默認實例構造器,為Rectangle分配內存時,內存中包含Point值類型的兩個實例。處于對性能的考慮,CLR不會為包含在引用類型中的每個值類型字段都調用一個構造器。但是,如前面提到的,值類型的字段會被初始化為0或null。
CLR是允許為值類型定義構造器。但執行這種構造器的唯一方式就是寫代碼來顯示地調用它們。如下:internal struct Point { // 在C#中,向一個值類型應用關鍵字new, // 可以調用構造器來初始化值類型的字段。 public Int32 m_x, m_y; public Point(Inte32 x, Int32 y) { m_x = x; m_y = y; }} internal sealed class Rectangle { public Point m_topLeft, m_bottomRight; public Rectangle() { m_topLeft = new Point (1, 2); m_bottomRight = new Point (100, 200); }} 好了,讓我們來改變下上面的沒有定義默認的無參構造器。
internal struct Point { public Int32 m_x, m_y; public Point() { m_x =m_y = 5; }} internal sealed class Rectangle { public Point m_topLeft, m_bottomRight; public Rectangle() { }} 現在我們來想象,m_x和m_y值是多少呢?是5嗎?還是0?
正確答案是0,為什么呢?因為代碼中沒有任何地方顯式調用過Point的構造器,即使值類型提供了無參構造器。更明顯的是,C#編譯器會報錯:error CS0568:結構不能包含顯式地無參構造器。 C#編譯器故意不允許值類型帶有無參構造器,旨在避免開發人員對這種構造器在什么時候調用產生迷茫。沒有無參構造器,值類型的字段總是被初始化為0或null。 嚴格的說,只有當值類型的字段嵌套到引用類型中,才保證會被初始化0或null。基于棧的值類型字段不保證為0或null。 注意,雖然C#編譯器不允許值類型帶有無參構造器,但是CLR允許。你可以使用其他語言(如IL匯編語言)定義帶有無參構造器的值類型。 由于C#是不允許為值類型定義無參構造器,所以編譯一下類型是,會報錯。internal struct SomeValType { //不能再值類型中內聯實例字段的初始化 private Int32 m_x = 5;} 為了生成"可驗證"的代碼,在訪問值類型的任何一個字段前,都需要對全部字段進行賦值,所以,值類型的任何構造器必須初始化值類型的全部字段。internal struct SomeValType { private Int32 m_x,m_y; //C#允許為值類型定義有參構造器 public SomeValType(Int32 x){ m_x = x; }} 編譯這段代碼時,C#編譯器會報錯,說字段"SomeValType.m_y"必須賦值。 為了修正這個問題,需要在構造器中為y賦一個值(通常為0)。 下面是對值類型的全部字段進行賦值的一個替代方案://C#允許為之類定義有參構造器public SomeValType(Int32 x){ //this代表本身實例,使用new操作符會將所有的字段初始化為0/null this = new SomeValType(); //使用x覆蓋m_x的0 m_x = x; //現在y已經初始化為0了} 注意,在引用類型的構造器中,this被認為是只讀的,所以不能對它賦值。
三、類型構造器
除了實例構造器,CLR還支持類型構造器(type constructor),也稱為靜態構造器(staticconstructor)、類構造器(classconstructor)等。
類型構造器可用于接口(C#不允許)、引用類型和值類型。 實例構造器的作用是設置類型的實例的初始狀態。類型構造器的作用是設置類型的初始狀態。 類型默認沒有定義類型構造器,如果定義,也只能定義一個,此外,類型構造器永遠沒有參數。 比如:internal sealed class SomeRefType { static SomeRefType () { //SomeRefType 被首次訪問時,執行這里的代碼。 }}internal struct SomeValType { //C#允許值類型定義無參構造器 static SomeValType () { //SomeValType 被首次訪問時,執行這里的代碼。 }} 類型構造器總是私有的,C#編譯器會自動把它們標記成private。之所以私有,是為了阻止任何有開發人員寫的代碼調用它,對它的調用總是由CLR負責的。
類型構造器的調用比較麻煩,JIT編譯器在編譯一個方法時,會檢查代碼中都引用了那些類型。任何一個類型定義了類型構造器,JIT編譯器都會檢查針對當前AppDomain,是否已經執行了這個類型構造器。CLR總要確保一個類型構造器只執行一次。為了保證這一點,在調用類型構造器時,調用線程要獲取一個互斥線程同步鎖。這樣一來,如果多個線程試圖調用某一個類型的類型構造器,只有一個線程可以獲得鎖,其他線程會被阻塞。第一個線程會執行靜態構造器中的代碼。當第一個線程離開構造器后,正在等待的線程將被喚醒,然后發現構造器中的代碼已經被執行過。因此,這些線程不會再次執行代碼,將直接從構造器方法返回。另外,如果再次調用這樣的一個方法,CLR知道該方法所在類型的類型構造器已經被執行過了,從而確保構造器不會再被調用。 雖然在值類型中能定義一個類型構造器,但不要這么做,因為CLR有時不會調用值類型的類型構造器。 由于CLR保證了一個類型構造器在每個AppDomian中只執行一次,而且這種執行時線程安全的,所以非常適合在類型構造器中初始化類型需要的任何單實例(Singleton)對象。 類型構造器中的代碼只能訪問類型的靜態字段,并且它的常規用途就是初始化這些字段。和實例構造器一樣,C#提供了簡單的原發來初始化類型的靜態字段。如;internal sealed class SomeType{ public static Int32 s_x = 5;} 雖然C#不允許值類型為它的實例字段使用內聯字段初始化語法,但可以為靜態字段使用。
類型初始化的性能: 在編譯一個方法時,JIT編譯器要決定是否在方法中生成一個對類型構造器的調用。如果JIT編譯器決定生成這個調用,它還必須決定將這個調用添加到什么位置。具體什么位置,有以下兩種可能: 1)JIT編譯器可以剛好在創建類型的第一個實例前,或者剛好在訪問類的一個非繼承的字段或成員之前生成這個調用。這稱為"精確"(precise)語義,因為CLR調用類型構造器的時機拿捏恰到好處。 2)JIT編譯器可能在首次訪問一個靜態字段或一個靜態/實例方法之前,或者在調用一個實例構造器之前,隨便找個時間生成。這稱為"字段初始化前"(before-field-init)語義,因為CLR只保證訪問成員之前會運行類型構造器,可能提前很早就允許了。 "字段初始化前"語義是首選的,因為它是CLR能夠自由選擇調用類型構造器的時間,而CLR會盡可能地利用這一點來生成運行得更快的代碼。 默認情況下,語言的編譯器會選擇對你定義的類型來說最恰當的一種語義,并在類型定義元數據表的行中設置beforefieldinit標識,從而告訴CLR這個選擇。 現在重點關注下C#編譯器具體如何讓選擇,以及這些選擇會對性能產生什么樣的影響,如下代碼:public sealed class TypeConstructorPerformance { public static void Go() { const Int32 iterations = 1000 * 1000 * 1000; PerfTest1(iterations); PerfTest2(iterations); } // 由于這個類沒有顯示定義類型構造器,所以C#在元數據中 // 用BeforeFieldInit來標記類型定義 internal s