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); //改掉了吗?没改掉
(附:在托管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拆箱!
更多c#装箱和拆箱知识整理相关文章请关注PHP中文网!