在CLR中為了將一個值類型轉換成一個引用類型,要使用一個名為裝箱的機制。
下面總結了對值類型的一個實例進行裝箱操作時內部發生的事: 1)在托管堆中分配好內存。分配的內存量是值類型的各個字段需要的內存量加上托管堆上的所有對象都有的兩個額外成員(類型對象指針和同步塊索引)需要的內存量。 2)值類型的字段復制到新的分配的堆內存。 3)返回對象的地址。現在,這個地址是對一個對象的引用,值類型現在是一個引用類型。 拆箱不是直接將裝箱過程倒過來。拆箱的代價比裝箱低得多。拆箱其實就是一個獲取一個指針的過程,該指針指向包含在一個對象中的原始值類型(數據字段)。事實上,指針指向的是已裝箱實例中的未裝箱部分。所以,和裝箱不同,拆箱不要求在內存中復制字節。還有一個重點就是,拆箱之后,往往會緊接著發生一次字段的復制操作。 一個已裝箱的值類型實例在拆箱時,內部會發生下面這些事情。 1.如果包含了"對已裝箱值類型實例的引用"的變量為null,就拋出一個NullReferenceException異常。 2.如果引用指向的對象不是所期待的值類型的一個已裝箱實例,就拋出一個InvalidCastException異常。 上面第二條意味著一下代碼不會如你預期的那樣工作:public static void Main(){ Int32 x = 5; Object o = x; Int16 y = (Int16) o;//拋出異常}在對一個對象進行拆箱的時候,只能將其轉型為原始未裝箱時的值類型——Int32,下面是正確的寫法:
public static void Main(){ Int32 x = 5; Object o = x; //對x進行裝箱,o引用已裝箱的對象 Int16 y = (Int16) (Int32) o; //先拆箱為正確的類型,在進行裝箱}前面說過,在進行一次拆箱后,經常會緊接著一次字段的復制。以下演示了拆箱和復制操作:
public static void Main() { Point p = new Point(); //棧變量 p.x = p.y = 1; object o = p; //對p進行裝箱,o引用已裝箱的實例 p = (Point) o; //對o進行拆裝,將字段從已裝箱的實例復制到棧變量}在最后一行,C#編譯器會生成一條IL指令對o進行拆箱,并生成另一條IL指令將這些字段從堆復制到基于棧的變量p中。
再看看一下代碼:
public static void Main() { Point p = new Point(); // 棧變量 p.x = p.y = 1; object o = p; // 對p進行裝箱,o引用已裝箱的實例 // 將Point的x字段變成2 p = (Point) o; // 對o進行拆裝,將字段從已裝箱的實例復制到棧變量 p.x = 2; // 更改變量的狀態 o = p; // 對p進行裝箱,o引用已裝箱的實例} 最后三行代碼唯一的目的就是將Point的x字段從1變成2.為此,首先要執行一次拆箱,在執行一次字段復制,在更改字段(在棧上),最后執行一次裝箱(從而在托管堆上創建一個全新的已裝箱實例)。希望你能體會到裝箱和拆箱/復制操作對應用程序性能的影響。
在看個演示裝箱和拆箱的例子:
PRivate static void Main(string[] args){ Int32 v = 5; // 創建一個偽裝箱的值類型變量 Object o = v; // o引用一個已裝箱的、包含值5的Int32 v = 123; // 將未裝箱的值修改成為123 Console.WriteLine(v + "," + (Int32)o); //顯示"123,5"}
你可以看出上述代碼進行了幾次裝箱操作?如果說是3次,你會不會意味呢?我們來看下生成的IL代碼。
.method private hidebysig static void Main(string[] args) cil managed{ .entrypoint //代碼大小 .maxstack 3 .locals init ( [0] int32 num, [1] object obj2) L_0000: nop // 將5加載到v中 L_0001: ldc.i4.5 L_0002: stloc.0 // 對v進行裝箱,將引用指針存儲到o中 L_0003: ldloc.0 L_0004: box int32 L_0009: stloc.1 // 將123加載到v中 L_000a: ldc.i4.s 0x7b L_000c: stloc.0 // 對v進行裝箱,并將指針保留在棧上以進行Concat(連接)操作 L_000d: ldloc.0 L_000e: box int32 // 將字符串加載到棧上以執行Concat操作 L_0013: ldstr "," // 對o進行拆箱:獲取一個指針,它指向棧上的Int32的字段 L_0018: ldloc.1 L_0019: unbox.any int32 // 對Int32進行裝箱,并將指針保留在棧上以進行Concat(連接)操作 L_001e: box int32 // 調用Concat L_0023: call string [mscorlib]System.String::Concat(object, object, object) // 將從Concat放回的字符串傳給WriteLine L_0028: call void [mscorlib]System.Console::WriteLine(string) L_002d: nop // 從Main返回 L_002e: ret }提示:主要原因是在Console.WriteLine方法上。
Console.WriteLine方法要求獲取一個String對象,為了創建一個String對象,C#編譯器生成的代碼來調用String對象的靜態方法Concate。該方法有幾個重載的版本,唯一區別就是參數數量,在本例中需要連接三個數據項來創建一個字符串,所以編譯器會選擇以下Concat方法來調用:
public static String Concat(Objetc arg0, Object arg1, Onject arg2);
所以,如果像下面寫對WriteLine的調用,生成的IL代碼將具有更高的執行效率:
Console.WriteLine(v + "," + o); //顯示"123,5"
這只是移除了變量o之前的(Int32)強制轉換。就避免了一次拆箱和一次裝箱。
我們還可以這樣調用WriteLine,進一步提升上述代碼的性能:
Console.WriteLine(v.ToString() + "," + o); //顯示"123,5"
現在,會為未裝箱的值類型實例v調用ToString方法,它返回一個String。String類型已經是引用類型,所以能直接傳給Concat方法,不需要任何裝箱操作。
下面在演示一個裝箱和拆箱操作:
private static void Main(string[] args){ Int32 v = 5; // 創建一個偽裝箱的值類型變量 Object o = v; // o引用一個已裝箱的、包含值5的Int32 v = 123; // 將未裝箱的值修改成為123 Console.WriteLine(v) //顯示"123" v = (Int32) o; //拆箱并將o復制到v Console.WriteLine(v); //顯示"5"}上述代碼發生了多少次裝箱呢?答案是一次。因為System.Console類定義了獲取一個Int32作為參數的WriteLine方法的重載版本:
public static String Concat(Int32 value);
在WriteLine方法內部也許會發生裝箱操作,但這已經不是我們能控制的。我們已經盡可能地從自己的代碼中消除了裝箱操作。
最后,如果知道自己寫的代碼會造成編譯器反復對一個值類型進行裝箱,請改用手動方式對值類型進行裝箱。
對象相等性和同一性。System.Object類型提供了一個名為Equals的虛方法,它的作用是在兩個對象包含相同的值得前提下返回true。如:
public class Object{ publick virtual Boolean Equals(Object obj) { //如果兩個引用指向同一個對象,它們肯定包含相同的值 if ( this == obj ) return true; //假定對象不包含相同的值 return false; }}對于Object的Equals方法的默認實現來說,它實現的實際是同一性,而非相等性。
下面展示了如何在內部正確實現一個Equals方法。 1)如果obj實參為null,就返回false,因為在調用非靜態的Equals方法時,this所標識的當前對象顯然不為null. 2)如果this和obj實參引用同一個對象,就返回true。在比較包含大量字段的對象時,這一步有助性能提升。 3)如果this和obj實參引用不同類型的對象,就返回false。一個String對象顯然不等于一個FileStream對象。 4)針對類型定義的每個實例字段,將this對象中的值與obj對象中的值進行比較。任何字段不相等,就返回false。 5)調用基類的Equals方法,以便比較它定義的任何字段。如果基類的Equals方法返回false,就返回false;否則返回true;例如:public class Object{ public virtual Boolean Equals(Object obj) { //要比較的對象不能為null if (obj == null ) return false; //如果對象類型不同,則肯定不相等 if (this.GetType() != obj.GetType()) return false; //如果對象屬于相同的類型,那么在它們所有字段都匹配的前提下返回true //由于System.Object沒有定義任何字段,所以字段是匹配的 return true; }}由于,一個類型能重寫Object的Equals方法,所以不能再調用這個Equals方法來測試同一性。為了修正這一問題,Object提供了一個靜態方法ReferenEquals,其原型如下:
public class Object{ public static Boolean ReferenceEquals(Object objA , Object objB) { retuen ( onjA == objB ); }}如果想檢查同一性,務必調用ReferenceEquals,而不應該使用C#的== 操作符,因為==操作符可能被重載。
System.ValueType(所有值類型的基類)重寫了Object的Equals方法,并進行了正確的實現來執行值得相等性檢查。新聞熱點
疑難解答