仔細了解裝箱和拆箱其實是很有趣的,首先來看為什麼會裝箱和拆箱呢?
看下面一段程式碼:
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) 值類型的裝箱與拆箱(一)的內容,更多相關內容請關注PHP中文網(www.php.cn)!