c#基礎語言編程-裝箱和拆箱

引言

爲什麼有裝箱和拆箱,兩者起到什麼作用?NET的所有類型都是由基類System.Object繼承過來的,包括最常用的基礎類型:int, byte, short,bool等等,就是說所有的事物都是對象。如果程序中所有的類型操作用的是引用類型時,往往導致效率低下,所以.Net通過將數據類型分爲值類型和引用類型。
前面文章中講過;

  • 值類型

定義:值類型是在棧中分配內存,在聲明時初始化後才能使用,不能爲null。
a、整型:(Sbyte、Byte、Char、Short、Ushort、Int、Uint、Long、Ulong)
b、浮點型:(Float、Double)、decima、bool
c、用戶定義的結構(struct)。

  • 引用類型

定義:引用類型是在託管堆中分配內存空間用於存儲數據、數據指針、以及Sync等,初始化默認爲null。
包括類、接口、委託、數組以及內置引用類型object與string。

存儲類型

什麼是堆,什麼是棧?
1、堆區(heap) 一般由程序員進行申請、釋放,若程序員不釋放,在程序退出時內存自動釋放。
2、棧區(statck)- 由編譯器自動分配釋放,存放函數的參數值,局部變量的值
3、全局區(靜態區)-全局變量和靜態變量的存儲是放在一塊的,初始化的全局變量和靜態變量在一塊區域, 未初始化的全局變量和未初始化的靜態變量在相鄰的另一塊區域。 - 程序結束後由系統釋放
4、文字常量區 —常量字符串就是放在這裏的。 程序結束後由系統釋放 ­
5、程序代碼區—存放函數體的二進制代碼。

裝箱和拆箱定義和過程

通俗上講,裝箱是講值類型轉化爲引用類型,
拆箱是將引用類型轉化爲值類型 。
將值類型與引用類型鏈接起來爲何需要裝箱?(爲何要將值類型轉爲引用類型?)
一種最普通的場景是,調用一個含類型爲Object的參數的方法,該Object可支持任意爲型,以便通用。當你需要將一個值類型(如Int32)傳入時,需要裝箱。
另一種用法是,一個非泛型的容器,同樣是爲了保證通用,而將元素類型定義爲Object。於是,要將值類型數據加入容器時,需要裝箱。

  • 裝箱操作(boxing):

1、首先從託管堆中爲新生成的引用對象分配內存。
2、然後將值類型的數據拷貝到剛剛分配的內存中。
3、返回託管堆中新分配對象的地址。
可以看出,進行一次裝箱要進行分配內存和拷貝數據這兩項比較影響性能的操作。

  • 拆箱操作(unboxing):

1、首先獲取託管堆中屬於值類型那部分字段的地址,這一步是嚴格意義上的拆箱。
2、將引用對象中的值拷貝到位於線程堆棧上的值類型實例中。
經過這2步,可以認爲是同boxing是互反操作。嚴格意義上的拆箱,並不影響性能,但伴隨這之後的拷貝數據的操作就會同boxing操作中一樣影響性能。

  • 代碼實例:

c#代碼

bject objValue = 4;
int value = (int)objValue;

上面的兩行代碼會執行一次裝箱操作將整形數字常量4裝箱成引用類型object變量objValue;然後又執行一次拆箱操作,將存儲到堆上的引用變量objValue存儲到局部整形值類型變量value中。
Il代碼

locals init (
  [0] object objValue,
  [1] int32 'value'
) //上面IL聲明兩個局部變量object類型的objValue和int32類型的value變量
IL_0000: nop
IL_0001: ldc.i4.4 //將整型數字4壓入棧
IL_0002: box [mscorlib]System.Int32  //執行IL box指令,在內存堆中申請System.Int32類型需要的堆空間
IL_0007: stloc.0 //彈出堆棧上的變量,將它存儲到索引爲0的局部變量中
IL_0008: ldloc.0//將索引爲0的局部變量(即objValue變量)壓入棧
IL_0009: unbox.any [mscorlib]System.Int32 //執行IL 拆箱指令unbox.any 將引用類型object轉換成System.Int32類型
IL_000e: stloc.1 //將棧上的數據存儲到索引爲1的局部變量即value

上述代碼中有幾個box意味着有幾次裝箱操作,有幾個unbox就是拆箱操作。
在拆箱中注意的問題:

int x = 0; ­
Int32 y = new Int32(); ­
Object o ; ­
o = x; //隱式的裝箱。
o = (Int32)y; //顯示的裝箱。

對於裝箱而言,是不存在任何疑問點,既可以用顯示(Explicit),也可以用隱式(Implicit)。 ­

x = o; //Error; ­
x = (int)o 或者 x = (Int32)o; //Right; ­

拆箱必須是顯示的,而不是隱式的。

Int32 x = 5; ­
Int64 y = 6; 
object o; 
o = x; or o = (Int32)x;
y = (Int64)o; //It's Error. ­

裝箱的類型必須與拆箱的類型一致。而不是什麼可隱式轉換之類的。所以裝箱的時候用的是Int32,拆箱的時候必須是Int32。 ­

避免裝箱的方法

通過泛型來避免。
- 非泛型集合

var array = new ArrayList();
array.Add(1);
array.Add(2); 
foreach (int value in array)
{
Console.WriteLine(“value is {0}”,value);
}

在向ArrayList中添加int類型元素時會發生裝箱,在使用foreach枚舉ArrayList中的int類型元素時會發生拆箱操作,將object類型轉換成int類型,在執行到Console.WriteLine時,還會執行兩次的裝箱操作;這一段代碼執行了6次的裝箱和拆箱操作;

  • 泛型集合
var list = new List<int>();
list.Add(1);
list.Add(2);

foreach (int value in list)
{
Console.WriteLine("value is {0}", value);
}

代碼和1中的代碼的差別在於集合的類型使用了泛型的List,而非ArrayList;我們同樣可以通過查看IL代碼查看裝箱拆箱的情況,上述代碼只會在Console.WriteLine()方法時執行2次裝箱操作,不需要拆箱操作。
通過重載函數來避免。

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(); 
} 

5.0:a.ToString()。編譯器發現A重寫了ToString方法,會直接調用ToString的指令。因爲A是值類型,編譯器不會出現多態行爲。因此,直接調用,不裝箱。(注:ToString是A的基類System.ValueType的方法)
5.1:a.GetType(),GetType是繼承於System.ValueType的方法,要調用它,需要一個方法表指針,於是a將被裝箱,從而生成方法表指針,調用基類的System.ValueType。(補一句,所有的值類型都是繼承於System.ValueType的)。
5.2:a.Clone(),因爲A實現了Clone方法,所以無需裝箱。
5.3:ICloneable轉型:當a2爲轉爲接口類型時,必須裝箱,因爲接口是一種引用類型。
5.4:c.Clone()。無需裝箱,在託管堆中對上一步已裝箱的對象進行調用。
有時我們可以提前進行裝箱或者拆箱操作
比如:

//當我們如下時:
for (int i = 0; i < arrList.Length; i++)
{
   // 
}
//我們更因該這樣:
int L = arrList.Length;
for (int i = 0; i < L; i++)
{
   // 
}

附:其實上面的基於一個根本的原理,因爲未裝箱的值類型沒有方法表指針,所以,不能通過值類型來調用其上繼承的虛方法。另外,接口類型是一個引用類型。對此,我的理解,該方法表指針類似C++的虛函數表指針,它是用來實現引用對象的多態機制的重要依據
凡事並不能絕對,假設你想改造的代碼爲第三方程序集,你無法更改,那你只能是裝箱了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章