1.實例構造器和類
構造器是允許將類型的實例化爲良好的狀態的一種特殊方法。
當創建一個類型的實例時:
1)爲實例的字段分配內存。
2)初始化對象的附加字段(類型對象指針和同步塊索引)。
3)調用類型的實例構造器來設置對象的初始狀態。
構造引用類型對象時,在調用實例構造器之前,爲對象分配的內存總是先被歸零,構造器沒有顯式重寫的所有字段保證只有一個0或null值。
和其他的方法不同,實例構造器永遠不能被繼承。
因爲實例構造器不能被繼承,類只有類自己定義的實例構造器,所以就不能用virtual,new,override,sealed,abstract修飾符來定義構造器。
如果定義的類沒有顯式的定義一個構造器,編譯器會默認的定義一個無參的構造器。在默認構造器的實現中,它只是簡單的調用了基類的無參構造器。
class Sometype { } class Sometype//這兩種定義是相等的。 { public Sometype() : base() { } }
如果類的修飾符是abstract,那麼編譯器生成的默認構造器的可訪問性就爲proctected。
如果基類沒有提供無參構造器,那麼派生類必須顯式調用一個基類構造器。
class Sometype { public Sometype(int x) { } void DoSome() { } } class Some : Sometype { public Some()//編譯出錯 { }
public Some():base(5)//通過
{
}
}
如果類的修飾符爲static,編譯器不會在類的定義中生成默認的構造器。
一個類型可以定義一個或多個實例構造器,每個構造器都必須有不同的簽名。而且可以有不同的可訪問性。
類的實例構造器在訪問從基類繼承的任何字段之前,必須首先調用基類的構造器。如果派生類的構造器沒有顯式調用一個基類構造器,編譯器會自動生成對默認的基類構造器的調用。最終,System.Object的公共無參構造器會得到調用,這個構造器嘛都不幹,直接返回。因爲System.Object沒有定義實例數據字段,所以它的構造器就沒事幹。
在極少的情況下可以在不調用基類構造器的前提下創建一個類型的實例,一個典型的例子是Object的MemberwiseClone方法。該方法的作用是分配內存,初始化對象的附加字段(你應該記得吧,類型對象指針和同步塊索引)
然後將源對象的字節複製到新對象中。還有就是用運行時序列化器(runtime seializer)反序列化對象時,通常也不需要調用構造器。反序列化代碼使用System.Runtime.Serialization.FormatterServices類型的GetUnintializedObject或者GetSafeuninitializedObject方法爲對象分配內存,期間不會調用一個構造器。
賓果(不要在構造器中調用會影響所構造對象的任何虛方法,原因是假如這個虛方法在當前要實例化的類型的派生類型中進行了重寫,就會調用重寫的實現,但是在繼承層次結構中,字段尚未完全初始化(派生類型的構造器還沒有調用)。)
來看看下面這個例子中字段初始化是怎麼發生的吧
sealed class Sometype { private Int32 x = 5;//sometype的構造器把5儲存在字段x,接着調用基類的構造函數。C#編譯器提供了一個簡化的語法。允許以內聯的方式初始化實例字段。但在幕後
//它會將這工語法轉換成構造器中的代碼來執行初始化。 }
sealed class Some { private Int32 x = 5; private string s = "there"; private Int32 i; public Some() { }//編譯器在爲這3個構造器方法生成代碼時,在每個方法開始的位置,都會包含用於初始化x,s,i的代碼。在這些 //代碼初始化之後,編譯器會插入對基類構造函數的調用,然後在插入自己的構造器代碼。 //在這裏,即使沒有顯式的初始化i,也會保證i會被初始化爲0. public Some(int x1) { x = 10; } public Some(string s1) { s = s1; } }
在上面的例子中,some類有3個構造器,所以編譯器公生成3此初始化x,s,i的代碼。如果有幾個已初始化的實例字段和大量重載的構造器方法可以考慮下面的方法:
sealed class Some { private Int32 x ; private string s ; private Int32 i; //該構造器將所有的字段都設爲默認值。 //其他的所有構造器都顯示調用這個構造器 public Some() { x = 30; s = "there"; i = 300; } //該構造器將所有的字段都設爲默認值,然後修改x的默認值 public Some(int x1) : this() { x = x1; } //同上。。。 public Some(string s1) : this() { s = s1; } }
2.實例構造器的結構(值類型)
值類型(struct)構造器的工作方式與引用類型的截然不同,CLR總是允許創建值類型的實例,並且阻止不了值類型的實例化。所以值類型不需要構造器,編譯器也根本不會爲值類型生成默認的構造器。
但是CLR允許爲值類型定義構造器,但如果要執行定義的構造器,必須要顯式的調用它們。
struct Point { private int x, y; public Point(int x1, int y1)//如果顯式聲明瞭構造函數,就必須要爲值類型的所有字段賦值。這是因爲爲了生成“可驗證”的代碼,在訪問值類型的任何一個字段之前,要對所有的字段進行賦值。 { x = x1; y = y1; } public Point()//結構體不能包含顯式的無參構造函數,這裏會編譯出錯! {//實際上,即使值類型提供哦你了無參構造器,許多編譯器都不會去生成代碼自動調用它,爲了執行值類型的無參構造器,開發人員必須顯式調用值類型的構造器。 x = 20; y = 30; }//C#編譯器故意不允許值類型定義無參構造器,就是爲了避免開發人員對這種構造器在什麼時候調用產生迷茫,由於不能定義無參構造器,所以編譯器永遠不會生成自動調用它的代碼。 } sealed class Rectangle { private Point p1, p2;//值類型的實例構造器只有在顯式調用的時候纔會執行, public Rectangle() { p1 = new Point(100, 20);//如果沒有用new操作符來調用Point的構造器,那麼Point的字段x,y都將爲0;這裏p1的x=100,y=200.p2的都爲0 } }
當一個值類型有很多字段時,如果一個一個的賦值會不會很麻煩呢。。下面是對值類型全部字段賦值的一個替代方案:
struct Point { public int x, y, z, q, n, m; public Some some; public Point(int x1, int y1)//允許爲值類型定義有參的構造器 { //字段會將所有的字段初始化爲0/null this = new Point(); x = x1;//用x1的值覆蓋x的0; y = y1;//用y1的值覆蓋y的0; } } class Program { public const Program p=null; static void Main() { Point p = new Point(200, 100); Some som= p.some;//這裏som爲null int x= p.x;//200 int y= p.y;//100 int z= p.z;//0 Console.ReadKey(); } }
在值類型中,this代表值類型本身的一個實例,用new創建類型的一個實例可以賦給this。在new的過程中,所有字段爲0/null。在引用類型中,this被認爲是隻讀的,不能賦值。
3.類型構造器
也就是靜態構造器,類構造器或者類型初始化器。類型構造器可用於接口(C#編譯器不允許,CLR允許),引用類型和值類型。
實例構造器的作用是設置類型的實例初始狀態。對應的,類型構造器的作用是設置類型的初始狀態。類型默認沒有定義類型構造器。如果自己定義,也只能定義一個。類型構造器永遠沒有參數!
sealed class Some { static Some()//可以看出,類型構造器類似於實例構造器,區別在於必須將它們標記爲static。 //類型構造器沒有訪問你修飾符,總是私有的,但是如果顯式的將類型標記爲private 編譯器會顯示:靜態構造函數不允許出現訪問修飾符。 //之所以私有,是爲了阻止任何用開發人員寫的代碼調用它,對類型構造器的調用總是由CLR負責的。 { } }
類型構造器的調用比較麻煩,JIT編譯器在編譯一個方法時,會查看代碼中都引用了那些類型。任何一個定義了類型構造器的類型,JIT編譯器都會檢查針對當前AppDomain,是否已經執行了這個類型的構造器。如果構造器從未執行,JIT編譯器會在它的本地(native)代碼中添加對類型構造器的一個調用。如果類型構造器已經執行,JIT編譯器就不添加對它的調用,因爲JIT編譯器知道類型已經初始化了。
現在,當方法被JIT編譯完畢後,線程開始執行它,最終會執行到調用哪個類型構造器的代碼。事實上,多個線程可能同時執行相同的方法。CLR希望確保在每個AppDomian(應用程序域)中,一個類型構造器只能執行一次,爲了保證這點,在調用類型構造器時,調用線程要獲取一個互斥同步鎖。這樣一來,在某一時間就會只用一個線程執行類型構造器中的代碼了,第一個線程執行類型構造器中的代碼,完事了第一個線程釋放鎖,當第一個線程離開構造器,正在等待的線程將被喚醒,然後發現類型構造器的代碼已經被執行過,它就不會在執行這些代碼,直接從構造器方法返回。
類型構造器中的代碼只能訪問類型的靜態字段。並且它的常規用途就是初始化這些字段。和上面的實例字段一樣,C#同樣提供了一個簡單的語法來初始化類型的靜態字段。
sealed class Rectangle { private static int i = 200; }
生成上訴代碼時,編譯器自動生成一個類型構造器,代碼等於:
sealed class Rectangle { private static int i; static Rectangle() { i = 200; } }
類型構造器不應調用基類型的類型構造器,因爲類型不可能有靜態字段是從基類型分享或繼承的。
雖然說在值類型中也能定義類型構造器,但是永遠都不要這麼做:
struct Point { static Point() { Console.WriteLine("這句話永遠都不會顯示");//因爲CLR不會調用值類型的靜態構造器。 } }
類型構造器的性能:
class Program { static void Main() { PerfTest1(1000000000); PerfTest2(1000000000); Console.ReadKey(); } //這個方法被jit編譯時,BeforeFieldInit和Precise類的類型構造器還沒被執行,所以對這些構造器的調用將嵌入這個方法的代碼中,使它的運行變得較慢。 private static void PerfTest1(int iterations) { Stopwatch sw = Stopwatch.StartNew(); for (int x = 0; x < iterations; x++) { //jit編譯器優化調用BeforeFileldInit的類型構造器的代碼,使它在循環之前執行。 BeforefieldInit.s_x = 1; } Console.WriteLine("PerfTest1:{0} BeforeFileldInit", sw.Elapsed); sw = Stopwatch.StartNew(); for (int x = 0; x < iterations; x++) { //jit編譯器在這裏生成調用Precise類的類型構造器的代碼,所以在每次循環迭代,都要覈實一遍是否需要調用構造器。 Precise.s_x = 1; } Console.WriteLine("PerfTest1:{0} Precise", sw.Elapsed); } //這個方法被jit編譯時,BeforeFieldInit和Precise類的類型構造器已經被執行,所以這個方法的代碼中,不會在生成對這些構造器的調用,它運行的更快。 private static void PerfTest2(int iterations) { Stopwatch sw = Stopwatch.StartNew(); for (int x = 0; x < iterations; x++) { BeforefieldInit.s_x = 1; } Console.WriteLine("PerfTest2:{0} BeforeFileldInit", sw.Elapsed); sw = Stopwatch.StartNew(); for (int x = 0; x < iterations; x++) { Precise.s_x = 1; } Console.WriteLine("PerfTest2:{0} Precise", sw.Elapsed); } } //由於這個類沒有顯示定義類型的構造器,所以在C# //元數據中用BeforeFieldInit(字段初始化前)來標記定義 internal sealed class BeforefieldInit { public static int s_x = 123; } //由於類中顯式定義了類型的構造器,所以沒有用BeforefieldInit來標記定義 internal sealed class Precise { public static int s_x; static Precise() { s_x = 123; } }
C#編譯器如果看到一個類(BeforeFieldInit)包含進行了內聯初始化的靜態字段,會在類型的類型定義表中生成一個添加了BeforeFieldInit元數據標記的記錄項,
C#編譯器如果看到一個類(Precise)包含顯式的類型構造器,就不會添加BeforeFieldInit元數據標記了。它的基本原理是:靜態字段只要在訪問之前初始化就可以了,具體什麼時候無所謂。而顯式類型構造器可能
包含具有副作用的代碼,所以需要在精確拿捏運行的時間。
從輸出看,這個決定對性能影響極大。