1、裝箱和拆箱是一個抽象的概念
2、裝箱是將實值型別轉換為參考型別 ;
拆箱是將參考型別轉換為實值型別
利用裝箱和拆箱功能,可通過允許實值型別的任何值與Object 類型的值相互轉換,將實值型別與參考型別連結起來
例如:
int val = 100; object obj = val; Console.WriteLine (“對象的值 = {0}", obj);
這是一個裝箱的過程,是將實值型別轉換為參考型別的過程
int val = 100; object obj = val; int num = (int) obj; Console.WriteLine ("num: {0}", num);
這是一個拆箱的過程,是將實值型別轉換為參考型別,再由參考型別轉換為實值型別的過程
註:被裝過箱的對象才能被拆箱
3、.NET中,資料類型劃分為實值型別和引用(不等同於C++的指標)類型,與此對應,記憶體配置被分成了兩種方式,一為棧,二為堆(注意:是託管堆)
實值型別只會在棧中分配。
參考型別分配記憶體與託管堆。
託管堆對應於記憶體回收。
4:裝箱/拆箱是什嗎?
裝箱:用於在記憶體回收堆中儲存實值型別。裝箱是實值型別到 object 類型或到此實值型別所實現的任何介面類型的隱式轉換。
拆箱:從 object 類型到實值型別或從介面類型到實現該介面的實值型別的顯式轉換。
5:為何需要裝箱?(為何要將實值型別轉為參考型別?)
一種最普通的情境是,調用一個含類型為Object的參數的方法,該Object可支援任意為型,以便通用。當你需要將一個實值型別(如Int32)傳入時,需要裝箱。
另一種用法是,一個非泛型的容器,同樣是為了保證通用,而將元素類型定義為Object。於是,要將實值型別資料加入容器時,需要裝箱。
6:裝箱/拆箱的內部操作
裝箱
對實值型別在堆中分配一個對象執行個體,並將該值複製到新的對象中。按三步進行。
新分配託管堆記憶體(大小為實值型別執行個體大小加上一個方法表指標和一個SyncBlockIndex)。
將實值型別的執行個體欄位拷貝到新分配的記憶體中。
返回託管堆中新指派至的地址。這個地址就是一個指向對象的引用了。
有人這樣理解:如果將Int32裝箱,返回的地址,指向的就是一個Int32。我認為也不是不能這樣理解,但這確實又有問題,一來它不全面,二來指向Int32並沒說出它的實質(在託管堆中)。
拆箱
檢查對象執行個體,確保它是給定實值型別的一個裝箱值。將該值從執行個體複製到實值型別變數中。
有書上講,拆箱只是擷取引用對象中指向實值型別部分的指標,而內容拷貝則是指派陳述式之觸發。我覺得這並不要緊。最關鍵的是檢查對象執行個體的本質,拆箱和裝箱的類型必需匹配,這一點上,在IL層上,看不出原理何在,我的猜測,或許是調用了類似GetType之類的方法來取出類型進行匹配(因為需要嚴格匹配)。
7:裝箱/拆箱對執行效率的影響
顯然,從原理上可以看出,裝箱時,產生的是全新的引用對象,這會有時間損耗,也就是造成效率降低。
那該如何做呢?
首先,應該盡量避免裝箱。
比如上例2的兩種情況,都可以避免,在第一種情況下,可以通過重載函數來避免。第二種情況,則可以通過泛型來避免。
當然,凡事並不能絕對,假設你想改造的代碼為第三方程式集,你無法更改,那你只能是裝箱了。
對於裝箱/拆箱代碼的最佳化,由於C#中對裝箱和拆箱都是隱式的,所以,根本的方法是對代碼進行分析,而分析最直接的方式是瞭解原理結何查看反編譯的IL代碼。
比如:在迴圈體中可能存在多餘的裝箱,你可以簡單採用提前裝箱方式進行最佳化。
8:對裝箱/拆箱更進一步的瞭解
裝箱/拆箱並不如上面所講那麼簡單明了
比如:裝箱時,變為引用對象,會多出一個方法表指標,這會有何用處呢?
我們可以通過樣本來進一步探討。
舉個例子:
Struct A : ICloneable{public Int32 x;public override String ToString() {return String.Format(”{0}”,x);}public object Clone() {return MemberwiseClone();}}static void main() { A a; a.x = 100; Console.WriteLine(a.ToString()); Console.WriteLine(a.GetType()); A a2 = (A)a.Clone(); ICloneable c = a2; Ojbect o = c.Clone(); }
a.ToString()。編譯器發現A重寫了ToString方法,會直接調用ToString的指令。因為A是實值型別,編譯器不會出現多態行為。因此,直接調用,不裝箱。(註:ToString是A的基類System.ValueType的方法)
a.GetType(),GetType是繼承於System.ValueType的方法,要調用它,需要一個方法表指標,於是a將被裝箱,從而產生方法表指標,調用基類的System.ValueType。(補一句,所有的實值型別都是繼承於System.ValueType的)。
a.Clone(),因為A實現了Clone方法,所以無需裝箱。
ICloneable轉型:當a2為轉為介面類型時,必須裝箱,因為介面是一種參考型別。
c.Clone()。無需裝箱,在託管堆中對上一步已裝箱的對象進行調用。
附:其實上面的基於一個根本的原理,因為未裝箱的實值型別沒有方法表指標,所以,不能通過實值型別來調用其上繼承的虛方法。另外,介面類型是一個參考型別。對此,我的理解,該方法表指標類似C++的虛函數表指標,它是用來實現引用對象的多態機制的重要依據。
9:如何更改已裝箱的對象
對於已裝箱的對象,因為無法直接調用其指定方法,所以必須先拆箱,再調用方法,但再次拆箱,會產生新的棧執行個體,而無法修改裝箱對象。有點暈吧,感覺在說繞口令。還是舉個例子來說:(在上例中追加change方法)
public void Change(Int32 x) { this.x = x; }
調用:
A a = new A(); a.x = 100; Object o = a; //裝箱成o,下面,想改變o的值((A)o).Change(200); //改掉了嗎?沒改掉
沒改掉的原因是o在拆箱時,產生的是臨時的棧執行個體A,所以,改動是基於臨時A的,並未改到裝箱對象。
(附:在託管C++中,允許直接取加拆箱時第一步得到的執行個體引用,而直接更改,但C#不行。)
那該如何是好?
嗯,通過介面方式,可以達到相同的效果。
實現如下:
interface IChange { void Change(Int32 x); } struct A : IChange { … }
調用:
((IChange)o).Change(200);//改掉了嗎?改掉了
為啥現在可以改?
在將o轉型為IChange時,這裡不會進行再次裝箱,當然更不會拆箱,因為o已經是參考型別,再因為它是IChange類型,所以可以直接調用Change,於是,更改的也就是已裝箱對象中的欄位了,達到期望的效果。
10、將實值型別轉換為參考型別,需要進行裝箱操作(boxing):
首先從託管堆中為新產生的引用對象分配記憶體
然後將實值型別的資料拷貝到剛剛分配的記憶體中
返回託管堆中新指派至的地址
可以看出,進行一次裝箱要進行分配記憶體和拷貝資料這兩項比較影響效能的操作。
將參考型別轉換為實值型別,需要進行拆箱操作(unboxing):
首先擷取託管堆中屬於實值型別那部分欄位的地址,這一步是嚴格意義上的拆箱。
將引用對象中的值拷貝到位於線程堆棧上的實值型別執行個體中。
經過這2步,可以認為是同boxing是互反操作。嚴格意義上的拆箱,並不影響效能,但伴隨這之後的拷貝資料的操作就會同boxing操作中一樣影響效能。
11、
NET的所有類型都是由基類System.Object繼承過來的,包括最常用的基礎類型:int, byte, short,bool等等,就是說所有的事物都是對象。
如果申明這些類型得時候都在堆(HEAP)中分配記憶體,會造成極低的效率!(箇中原因以及關於堆和棧得區別會在另一篇裡單獨得說說!)
.NET如何解決這個問題得了?正是通過將類型分成值型(value)和引用型(regerencetype),
C#中定義的實值型別和參考型別
實值型別:原類型(Sbyte、Byte、Short、Ushort、Int、Uint、Long、Ulong、Char、Float、Double、Bool、Decimal)、枚舉(enum)、結構(struct)
參考型別:類、數組、介面、委託、字串等
值型就是在棧中分配記憶體,在申明的同時就初始化,以確保資料不為NULL;
引用型是在堆中分配記憶體,初始化為null,引用型是需要GARBAGE COLLECTION來回收記憶體的,值型不用,超出了作用範圍,系統就會自動釋放!
下面就來說裝箱和拆箱的定義!
裝箱就是隱式的將一個值型轉換為引用型對象。比如:
int i=0;Syste.Object obj=i;
這個過程就是裝箱!就是將i裝箱!
拆箱就是將一個引用型對象轉換成任意值型!比如:
int i=0;System.Object obj=i;int j=(int)obj;
這個過程前2句是將i裝箱,後一句是將obj拆箱!