剛開始學C#的時候就聽過裝箱和拆箱這兩個名詞,但是一直沒有去了解這兩個詞是什麼個意思,最近閑下來突然想到這個問題,就學了一發來blog上裝裝逼。
值類型和引用類型
在C#中,任何類型都直接或間接繼承于class object
通常我們将變量的類型分為值類型和引用類型,它們在 [記憶體位置 傳值/指派方式] 等方面有着較大差別
– 記憶體位置
值類型的變量所處的記憶體位置在目前線程棧/函數棧中
引用類型的變量,實際内容處在托管堆中,在new時在托管堆中申請所需空間并執行一系列初始化動作;而我們可以将在代碼中聲明的某一變量視作指向該位置的指針,它将在目前線程棧/函數棧中占據一定空間
– 傳值/指派方式
簡單了解,傳值和指派從某種角度上可以說是類似的,在調用函數時的傳值可以視作發生了從實參到形參的指派(雖然從彙編的角度看來兩者是不同的)
值類型的傳值/指派方式是對對應記憶體的直接複制
兩個int之間的指派,是4個位元組記憶體的複制,兩個不同的變量實際是兩個完全不同互不影響的變量,是以這裡出現了經典的在C語言教學中幾乎都會講的問題:
void swap(int a, int b);
void swap(int *a, int *b);
兩個引用類型的變量的指派,可視作指針之間的複制,即在發生類似以下情況時:
RefType a = new RefType();
RefType b = a;
a和b兩個變量所對應的在托管堆中的實際記憶體是同一塊,但是在棧中的變量指針部分所處的記憶體位置是不同的
裝箱和拆箱
裝箱和拆箱問題出現在值類型變量中(引用類型變量總是處于已裝箱狀态)
值類型比引用類型更輕,它們的内容不在托管堆上配置設定,也不涉及到垃圾回收的問題
首先我們知道在C#中,将子類變量指派給父類變量是允許的,而object是所有類型的基類,包括值類型
是以對于以下代碼
Int32 intval = ;
object obj = intval;
我們将值類型變量指派給引用類型變量,值類型與引用類型的上文提到過的各種差別使得這樣的指派看似非常沖突
在上面的代碼中,obj是一個引用類型的變量,但是指派的對象是一個值類型變量,是以這裡需要将在棧中的值類型變量轉換成一個在堆中托管的對象
裝箱提供了将值類型變量轉換成引用類型變量的處理方式
裝箱需要三個步驟
1. 在托管堆中申請記憶體(值類型大小+類型對象指針+同步塊索引)
2. 将值類型的各字段複制到新配置設定的堆記憶體中
3. 傳回該引用對象的位址
Int32 newval = 1;
Object obj = newval;
在上述代碼中,我們将引用類型的變量轉換為值類型
不管是不是新聲明的變量,都需要一個拆箱的過程
在拆箱時,堆中的引用類型變量中的各個字段會被複制到棧中值類型對應的記憶體上
拆箱不是裝箱的一個逆過程,拆箱的開銷比裝箱低得多
看一個例子
Int32 val = ;
Object obj = val;
Console.WriteLine(val + ", " + (Int32) obj);
在第二行中,發生了一次裝箱
在第三行中發生了兩次裝箱
對于
val + ", " + (Int32) obj
, 調用的是string的靜态方法
public static string Concat(Object arg0, Object arg1, Object arg2);
發生以下傳值
Object arg0 = val;
Object arg1 = ", ";
Object arg2 = (Int32) obj;
由此可以看出,在第一行發生一次裝箱,在第三行發生一次拆箱和裝箱。
裝箱需要額外的開銷,是以會降低處理效率
如果采用以下的代碼,程式效率将會更高