一種經常發生的裝箱
Int32 i = 100;Console.WriteLine("The number is: " + i);
通過VS SDK Tools裡的IL DASM工具看看產生的IL代碼:
.method private hidebysig static void Main(string[] args) cil managed{ .entrypoint // Code size 27 (0x1b) .maxstack 2 .locals init ([0] int32 i) IL_0000: nop IL_0001: ldc.i4.s 100 IL_0003: stloc.0 IL_0004: ldstr "The number is: " IL_0009: ldloc.0 IL_000a: box [mscorlib]System.Int32 IL_000f: call string [mscorlib]System.String::Concat(object, object) IL_0014: call void [mscorlib]System.Console::WriteLine(string) IL_0019: nop IL_001a: ret} // end of method Program::Main
可以發現在IL_000a行有一個box裝箱操作. 這主要是因為Console.WriteLine方法是輸出一個字串, 這時我們輸入了帶+號的計算式, 會調用String.Concat(Object arg0, Object arg1)的方法, 如此以來剛剛的Int32資料會被裝箱成一個Object資料.
完成一次裝箱的步驟
1. 新分配託管堆記憶體(大小為實值型別執行個體大小加上一個方法表指標和一個SyncBlockIndex)
2. 將實值型別的執行個體欄位拷貝到新分配的記憶體中
3. 返回託管堆中新指派至的引用地址
避免這樣的裝箱
裝箱就像給一件物品打包, 這需要一點時間, 上面的代碼裝箱時間可以忽略不計, 但如果這樣的代碼出現在一個迴圈次數比較多的中就需要改進一下. 但避免這樣的裝箱很簡單, 把上面兩行代碼改成這樣:
Int32 i = 100;Console.WriteLine("The number is: " + i.ToString());
代碼只是簡單的將Int32變成一個String類型(參考型別), 有人懷疑ToString()方法會執行一次裝箱, 因為他們覺得i是一個實值型別, 而String是一個參考型別. 但可以查看這兩句產生的IL代碼看看有沒有發生裝箱:
.method private hidebysig static void Main(string[] args) cil managed{ .entrypoint // Code size 28 (0x1c) .maxstack 2 .locals init ([0] int32 i) IL_0000: nop IL_0001: ldc.i4.s 100 IL_0003: stloc.0 IL_0004: ldstr "The number is: " IL_0009: ldloca.s i IL_000b: call instance string [mscorlib]System.Int32::ToString() IL_0010: call string [mscorlib]System.String::Concat(string, string) IL_0015: call void [mscorlib]System.Console::WriteLine(string) IL_001a: nop IL_001b: ret} // end of method Program::Main
可以發現ToString()方法並不會產生任何box裝箱操作的, 僅僅是實值型別獲得獲得值的字串表現形式罷了.
實值型別與參考型別之間的轉換
在使用new關鍵字建立一個參考型別對象的時候, 這個對象總是存在在託管堆裡, 返回的是指向這個對象的指標. 每一次建立參考型別的執行個體, 都需要從託管堆中分配記憶體, 記憶體回收機制會管理著這些記憶體. 如果每種類型都被這樣管理著, 這種機制會對程式的效能產生一些負面影響, 因此對於那些經常使用的簡單類型, CLR把他們歸於實值型別, 它們被分配在堆棧上.
所有被稱為”類”的都是參考型別! 特別注意的是System.String, 它也是個類, 它也是參考型別, 由於一種”字串駐留”技術, 使它成為了”擁有實值型別特點的參考型別”. 而結構或者枚舉類型都是實值型別, 比如Int32它也只不過是一個struct罷了.
實值型別因為不受記憶體回收機制等等作用, 在某些情況下可以獲得更好的效能. 但如果實值型別的執行個體如果經常被某Class經常調用比如被放到List<T>之類的集合(也是類)中, 程式會開闢另外的記憶體, 把該實值型別執行個體的值拷貝到該記憶體裡…這樣做會影響到效能.
因此我個人覺得值在下面兩個情況下拷貝了, 並且我們本不太希望這樣的事情發生:
1. 方法傳遞的參數類型是Object類型. 當然這樣的做法是為了能夠相容其它各種類型的參數, 不過通過可以重載這樣的方法避免一次實值型別->Object類型的操作.
2. 實值型別資料被某個Class使用了.
記憶體何時被釋放
實值型別的變數在範圍結束後就自動釋放了, 而參考型別都需要通過記憶體回收機制來釋放記憶體.
但是, Stream也是一個類, 按道理它產生的執行個體也受託管代碼管理, 並有記憶體回收機制對它的資源(記憶體)進行回收. 但我們還需要輸入一遍xxStrean.Close()和xxStream.Dispose(), 原因是記憶體回收的回收具有不確定性. 如果不寫xxStream.Dispose(), CLR的確在某個時刻也會回收它的資源, 只不過出於以下兩點考慮, 我們需要輸入xxStream.Dispose():
1. 針對Stream類, 記憶體資源比較有限, 需要及時得釋放已經確定不需要再使用的資源. 其他的比如網路連接的資源同樣如此.
2. Stream開啟的資源大多是獨享的, 在它沒被釋放之前, 如果其它的代碼試圖再次開啟這個資源, 會拋出異常
當然如果覺得寫xxStrean.Close()和xxStream.Dispose()比較煩的話, C#提供了using語句塊的用法:
using(FileStream fs = new FileStream(......)){ //......}
上面代碼中的fs會在using語句塊結束前得到及時的釋放. 當然using後面()中的對象需要實現IDisposable 介面, 這個介面裡面提供了Dispose()方法.