仔細瞭解裝箱和拆箱其實是很有趣的,首先來看為什麼會裝箱和拆箱呢?
看下面一段代碼:
class Program { static void Main(string[] args) { ArrayList array = new ArrayList(); Point p;//分配一個 for (int i = 0; i < 5; i++) { p.x = i;//初始化值 p.y = i; array.Add(p);//裝箱 } } } public struct Point { public Int32 x; public Int32 y; }
迴圈5次,每次都初始化一個Point實值型別欄位,然後放到ArrayList中。Struct是一個實值型別的結構,那麼ArrayList中存的什麼呢?我們再看一下ArrayList的Add方法。MSDN中可以看到Add方法:
public virtual int Add(Object value),
可以看出Add的參數是Object類型,也就是它需要的參數是一個對象的引用。也就是說這裡的參數必須是參考型別。至於何為參考型別,就不必細說了,無非就是堆上的一個對象的引用。不過在這裡為了方便理解,再次說一下堆和棧。
1、棧區(stack)— 由編譯器自動分配釋放 ,存放函數的參數值,局部變數的值等。
2、堆區(heap)— 由程式員分配釋放, 若程式員不釋放,程式結束時可能由OS回收。
例如下面:
class Program { static void Main(string[] args) { Int32 n;//這是實值型別,存放在棧中,Int32初始值為0 A a;//此時在棧中開闢了空間 a = new A();//真正執行個體化後的一個對象則儲存在堆中。 } } public class A { public A() { } }
再回到上面的問題中,Add方法需要參考型別的參數,怎麼辦呢?那就要用到裝箱,所謂裝箱,就是將一個實值型別轉換為一個參考型別。轉換的過程是這樣的:
1、在託管堆中分配好記憶體。分配的記憶體量是實值型別的各個欄位需要的記憶體量加上託管堆的所有對象都有的兩個額外成員(類型對象指標和同步塊索引)需要的記憶體量。
2、實值型別的欄位複製到新分配的對記憶體。
3、返回對象的地址。此時,這個地址是對一個對象的引用,實值型別現在已經轉換為了一個參考型別。
這樣,在Add方法中,儲存的是一個被裝箱的Point對象的引用。裝箱後的這個對象會一直在堆中,知道程式員處理或者系統記憶體回收。這時,已裝箱的實值型別的生存周期超過了未裝箱的實值型別的生存周期。
有了上面的裝箱,自然就需要拆箱了,如果要取出array的第0個:
Point p = (Point)array[0];
這裡要做的是,擷取ArrayList的元素0的引用,將其放到Point實值型別p中。為了達到這個目的,如何?呢,首先,擷取已裝箱的Point對象的各個Point欄位的地址。這就是拆箱。然後,將這些欄位包含的值從堆中複製到基於棧的實值型別執行個體中。拆箱其實就是擷取一個引用的過程,該引用指向包含在一個對象中的原始實值型別。事實上,引用指向的是已裝箱執行個體中的未裝箱部分。因此和裝箱不同,拆箱不需要在記憶體中複製任何位元組。不過還有一點,拆箱後緊接著發生一次欄位的複製操作。
所以裝箱和拆箱會對程式的速度和記憶體消耗造成不利影響,所以要注意什麼時候程式會自動進行裝箱/拆箱操作,在寫代碼時要盡量避免這些情況。
拆箱時,要注意下面的異常:
1、如果包含了“對已裝箱實值型別執行個體的引用”的變數為null,會拋出NullReferenceException。
2、如果引用指向的對象不是所期待的實值型別的已裝箱執行個體,會拋出InvalidCastException。
例如如下程式碼片段:
Int32 x = 5; Object o = x; Int16 r = (Int16)o;//拋出InvalidCastException異常
因為拆箱時候只能將其轉換為原來未裝箱時的實值型別。對上述代碼修改為:
Int32 x = 5; Object o = x; //Int16 r = (Int16)o;//拋出InvalidCastException異常 Int16 r = (Int16)(Int32)o;
此時正確。
在拆箱後,會發生一次欄位複製,如下代碼:
//會發生欄位複製 Point p1; p1.x = 1; p1.y = 2; Object o = p1;//裝箱,發生複製 p1 = (Point)o;//拆箱,並將欄位從已裝箱的執行個體複製到棧中
再看如下程式碼片段:
//要改變已裝箱的值 Point p2; p2.x = 10; p2.y = 20; Object o = p2;//裝箱 p2 = (Point)o;//拆箱 p2.x = 40;//改變棧中變數的值 o = p2;//再一次裝箱,o引用新的已裝箱執行個體
這裡的目的是要將裝箱後的p2的x值改為40,這樣,就需要先拆一次箱,執行一次複製欄位到棧中,在棧中改變欄位的值,然後執行一次裝箱,這時又要在堆上建立一個全新的已裝箱執行個體。由此也我們也看到裝箱/拆箱和複製對程式效能的影響。
下面再看幾個裝箱拆箱的程式碼片段:
//裝箱拆箱示範 Int32 v = 5; Object o = v; v = 123; Console.WriteLine(v + "," + (Int32)o);
這裡發生了3次裝箱,可明顯看出的是
Object o = v; v = 123;
但是在Console.WriteLine裡還發生了一次裝箱,為什麼呢?因為這裡的WriteLine中是string類型的參數,而string大家都知道是參考型別的,所以(Int32)o在這裡還要進行一次裝箱。在這裡再次說明了在程式中使用+號連接字串的問題,串連的時候有幾個實值型別,那麼就要進行幾次裝箱操作。
不過,上述代碼可以修改:
//修改後 Console.WriteLine(v.ToString() + "," + o);
這樣就沒有裝箱了。
再看如下代碼:
Int32 v = 5; Object o = v; v = 123; Console.WriteLine(v); v = (Int32)o; Console.WriteLine(v);
這裡只發生了一次裝箱,即Object o = v這裡,而Console.WriteLine由於重載了int,bool,double等,所以這裡並不發生裝箱。
以上就是C#基礎知識整理 基礎知識(18) 實值型別的裝箱和拆箱(一)的內容,更多相關內容請關注topic.alibabacloud.com(www.php.cn)!