知識點
- 值類型。
- 值類型是在棧中分配內存,在聲明時初始化才能使用,不能爲null。
- 值類型超出作用範圍系統自動釋放內存。
- 主要由兩類組成:結構,枚舉(enum),結構分爲以下幾類:
- 整型(Sbyte、Byte、Char、Short、Ushort、Int、Uint、Long、Ulong)
- 浮點型(Float、Double)
- decimal
- bool
- 用戶定義的結構(struct)
- 引用類型。
- 引用類型在堆中分配內存,初始化時默認爲null。
- 引用類型是通過垃圾回收機制進行回收。
- 包括類、接口、委託、數組以及內置引用類型object與string。
概念
由於C#中所有的數據類型都是由基類System.Object繼承而來的,所以值類型和引用類型的值可以通過顯式(或隱式)操作相互轉換,而這轉換過程也就是裝箱(boxing)和拆箱(unboxing)過程。
- 裝箱 是值類型到 object 類型或到此值類型所實現的任何接口類型的隱式轉換。對值類型裝箱會在堆中分配一個對象實例,並將該值複製到新的對象中。
- 拆箱 (取消裝箱)是從 object 類型到值類型或從接口類型到實現該接口的值類型的 顯式 轉換。取消裝箱操作包括:
示例
首先寫個簡單的控制檯程序:
// 裝箱與拆箱
using System;
class App
{
static void Main()
{
int i = 32;
object o = i; //隱式裝箱
Console.WriteLine("o = {0}", o);
Console.Read();
}
}
其中object o = i這裏我們進行了裝箱操作,然後我們用MSIL 反彙編程序查看下生成的.exe程序的內部機理。
2 {
3 .entrypoint
4 // 代碼大小 30 (0x1e)
5 .maxstack 2
6 .locals init ([0] int32 i,
7 [1] object o)
8 IL_0000: nop
9 IL_0001: ldc.i4.s 32
10 IL_0003: stloc.0
11 IL_0004: ldloc.0
12 IL_0005: box [mscorlib]System.Int32
13 IL_000a: stloc.1
14 IL_000b: ldstr "o = {0}"
15 IL_0010: ldloc.1
16 IL_0011: call void [mscorlib]System.Console::WriteLine(string,
17 object)
18 IL_0016: nop
19 IL_0017: call int32 [mscorlib]System.Console::Read()
20 IL_001c: pop
21 IL_001d: ret
22 } // end of method App::Main
其中第12行是我們的裝箱操作。(關於IL中出現的操作符代表的操作請查閱MSDN Library中的.NET開發/.NET Framework SDK/類庫參考/System.Reflection.Emit/OpCodes 類/OpCodes 字段)
然後我們取消裝箱操作:
{
int i = 32;
Console.WriteLine("i = {0}", i);
Console.Read();
}
再用MSIL工具查看生成的.exe,如下結果:
{
.entrypoint
// 代碼大小 28 (0x1c)
.maxstack 2
.locals init ([0] int32 i)
IL_0000: nop
IL_0001: ldc.i4.s 32
IL_0003: stloc.0
IL_0004: ldstr "i = {0}"
IL_0009: ldloc.0
IL_000a: box [mscorlib]System.Int32
IL_000f: call void [mscorlib]System.Console::WriteLine(string,
object)
IL_0014: nop
IL_0015: call int32 [mscorlib]System.Console::Read()
IL_001a: pop
IL_001b: ret
} // end of method App::Main
在IL_000a行,我們發現這裏卻也出現了一個box!不過這步是在call System.Console::WriteLine(string, object)時發生的。我們對比前面我們手動boxing的IL代碼,發現在我們手動boxing後就沒有這步box了。爲什麼呢?
當我們在調用一些方法的重載版本時,由於編譯器找不到符合給定參數類型的重載方法,此時編譯器便去尋找到的最接近的版本,然後使用找到的方法,而其參數卻是我們傳入的值類型的基類如System.Object或者其實現的接口類型,接着編譯器爲了求得與這個方法的原型一致,就必須對該值類型進行裝箱操作(轉換成引用類型)。
照這個說法當我們不手動boxing時,在調用了Console.WriteLine()方法輸出一個Int32類型值時,系統就要自動進行boxing。也就是說如果我們要對該輸出操作作5000次的循環,系統就要做5000次的boxing。這樣對性能便會有一定的影響,而且要使循環次數是100,000,000次呢,或者跟多!
此時我們便要想如何消除這不應該的性能損失!正如第一個程序是展示的,我們可以在需要的地方先進行boxing,這個原理很簡單,我們可以聯想到類似的做法:
for (int i = 0; i < arr.Length; i++)
{
//
}
//我們更因該這樣:
int L = arr.Length;
for (int i = 0; i < L; i++)
{
//
}
這樣,我們只要一次boxing,就可以避免讓系統重複的做這個操作。
用途
像在調用Console.WriteLine()的過程中系統自動進行boxing一樣,當我們在調用其它的一些方法的重載版本進行操所時,爲了避免由於無謂的隱式裝箱所造成的性能損失,在執行這些多類型重載方法之前,最好先對值進行裝箱。一般是在處理大量數據需要對類型進行裝箱操作。