C#構造器的那點事

來源:互聯網
上載者:User

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中繼資料標記了。它的基本原理是:靜態欄位只要在訪問之前初始化就可以了,具體什麼時候無所謂。而顯式類型構造器可能
包含具有副作用的代碼,所以需要在精確拿捏啟動並執行時間。
從輸出看,這個決定對效能影響極大。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.