Visual C#裝箱與拆箱

iDotNetSpace發表於2010-03-17
在對這個問題展開討論之前,我們不妨先來問這麼幾個問題,以系統的瞭解我們今天要探究的主題。

  觀者也許曾無數次的使用過諸如System.Console類或.NET類庫中那些品種繁多的類。那麼,我想問的是它們究竟源自何處?C#又是如何聯絡它們?有沒有支援我們個性化擴充套件的機制或型別系統?又有哪些型別系統可供我們使用呢?如果我們這些PL們連這些問題都不知其然,更不知其所以然的話,C#之門恐怕會把我們拒之門外的。

  那就讓我們先停停手中的活兒,理理頭緒,對作為.NET重要技術和基礎之一的CTS(Common Type System)做一個饒有興趣的研究。顧名思義,CTS就是為了實現在應用程式宣告和使用這些型別時必須遵循的規則而存在的通用型別系統。在這要插一句,雖然也許大家都對此再熟悉不過了,但是我還是要強調,.Net將整個系統的型別分成兩大類 —— 值型別 和 引用型別。到此,你也許會怒斥:說了這麼半天,你似乎還沒有切入正題呢!別慌!知道了.Net型別系統的的特點並不代表你真正理解了這個型別系統的原理和存在的意義。

  大多數物件導向的語言都有兩種型別:原型別(語言固有的型別,如整數、列舉)和類。雖然在實現模組化和實體化方面,物件導向技術體現了很強的能力,但是也存在一些問題,比如現在提到的這個
系統型別問題,歷史告訴我們兩組型別造成了許多問題。首先就是相容性問題,這個也是Microsoft使勁抨擊的一點,多數的OO語言存在這個弱點,原因就是因為他們的原型別沒有共同的基點,於是他們在本質上並不是真正的物件,它們並不是從一個通用基類裡派生來的。怪不得,Anders Heijlsberg 笑稱其為“魔術型別”。

  正是由於這一缺陷,當我們希望指定一個可以接受本語言支援的任何型別的引數的Method時,同樣的問題再次襲擾我們的大腦——不相容。當然,對於C++的PL大拿,也許這個沒有什麼大不了的,他們會自豪的說,只要用過載的構造器為每一種原型別編寫一個Wrapper Class 不就完了嘛!好吧,這樣總算是能共存了,但是,接下來我們怎麼從這個魔術中得到我們最關心的東東 —— 結果呢?於是,他們依然會自信的開啟Boarland,熟練的編寫一個過載過的函式來從剛才的那個 Wrapper Class 中獲取結果。兄弟 or 姐妹們 ,在當時的歷史條件下,你們的行為是創舉,但是相對於現在,你將會為此付出代價 —— 效率低下。畢竟,C++更依賴於物件,而非物件導向。承認現實總比死要面子更理智一些!花這麼大力氣,總算把鋪墊說完了,我想說的是:.Net環境的CTS 給我們帶來了方便。第一、CTS中的所有東西都是物件;第二、所有的物件都源自一個基類——System.Object型別。這就是所謂的單根層次結構(singly rooted hierarchy)關於System.Object的詳細資料請參考
微軟的技術文件。這裡我們簡略的談談上面提到過的兩大型別:Value Type 和 Reference Type。

  CTS值型別的一個最大的特點是它們不能為null,言外之意就是值型別的變數總有一個值。在C#中,它包括有原型別、結構、列舉器。這裡需要強調一點:在傳遞值型別的變數時,我們實際傳遞的是變數的值,而非底層物件的引用,這一點和傳遞引用型別的變數的情況截然不同;CTS引用型別就好像是型別安全的指標,它可以為null。它包括 如類、介面、委託、陣列等型別。對比前面值型別的特點,當我們分配一個引用型別時,
系統會在後臺的堆疊上分配一個值(記憶體分配與位置)並返回對這個值的引用;當值為null時,說明沒有引用或型別指向某個物件。這就意味著,我們在宣告一個引用型別的變數時,被操作的是此變數的引用(地址),而不是資料。

  討論到這個地方的時候,本篇的主角終於閃亮登場了——欲吐血或者嘔吐的同志,請再忍耐一下。我想問一個問題先:在使用這種多型別
系統時如何有效的擴充和提高系統的效能?也許就是在黑板上對這個問題的探討,西雅圖的那幫傢伙們提出了Box(裝箱) and UnBox(拆箱) 的想法。簡單的說。裝箱就是將值型別(value type)轉換為引用型別(reference type)的過程;反之,就是拆箱。(其實這種思想早八輩子就產生了)。下面我們就進一步詳細的討論裝箱和拆箱的過程。在討論中,我們剛剛提到的問題的答案也就迎刃而解了。



  首先,我們先來看看裝箱過程,為此我們需要先做兩個工作:1、編寫例程; 2、開啟ILDASM(MSIL程式碼察看工具)為此我們先來看看以下的程式碼:

using System;

namespace StructApp
{
///
/// BoxAndUnBox 的摘要說明。
///
public class BoxAndUnBox
{
public BoxAndUnBox()
{
//
// TODO: 在此處新增建構函式邏輯
//
}
/////////////////////////////////////////////////////////////////////////////////////
static void Main(string[] args)
{
double dubBox = 77.77; /// 定義一個值形變數
object bjBox = dubBox; /// 將變數的值裝箱到 一個引用型物件中
Console.WriteLine("The Value is '{0}' and The Boxed is {1}",dubBox,objBox.ToString());
}
/////////////////////////////////////////////////////////////////////////////////////
}
}

  程式碼中,本篇我們只需要關注Main()方法下加註釋的兩行程式碼,第一行我們建立了一個double型別的變數(dubBox)。顯然按規則,CTS規定double是原型別,所以dubBox自然就是值型別的變數;第二行其實作了三個工作,這個將在下面的MSIL程式碼中看的一清二楚。第一步取出dubBox的值,第二步將值型別轉換引用型別,第三步傳值給objBox。

  MSIL程式碼如下:

.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// 程式碼大小 40 (0x28)
.maxstack 3
.locals init ([0] float64 dubBox,
[1] object objBox)
IL_0000: ldc.r8 77.769999999999996
IL_0009: stloc.0
IL_000a: ldloc.0
IL_000b: box [mscorlib]System.Double
IL_0010: stloc.1
IL_0011: ldstr "The Value is '{0}' and The Boxed is {1}"
IL_0016: ldloc.0
IL_0017: box [mscorlib]System.Double
IL_001c: ldloc.1
IL_001d: callvirt instance string [mscorlib]System.Object::ToString()
IL_0022: call void [mscorlib]System.Console::WriteLine(string,
object,
object)
IL_0027: ret
} // end of method BoxAndUnBox::Main

  在MSIL中,第IL_0000 至 IL_0010 行是描述前面兩行程式碼的。參照C#的MSIL手冊,觀者不難理解這段底層程式碼的執行過程,在這我著重描述一下當dubBox被裝箱時所發生的故事:(1)劃分堆疊記憶體,在堆疊上分配的記憶體 = dubBox的大小 + objBox及其結構所佔用的空間;(2)dubBox的值(77.7699999999996)被複制到新近分配的堆疊中;(3)將分配給objBox的地址壓棧,此時它指向一個object型別,即引用型別。

  拆箱作為裝箱的逆過程,看上去好像很簡單,其實裡面多了很多值的思考的東西。首先,box的時候,我們不需要顯式的型別轉換,但是在unbox時就必須進行型別轉換。這是因為引用型別的物件可以被轉換為任何型別。(當然,這也是電腦和人腦一個差別的體現)型別轉換不容迴避的將會受到來自CTS管理中心的監控——其標準自然是依據規則。(其內容的容量足以專門設一章來討論)好了,我們還是先來看看下面這段程式碼吧:

using System;

namespace StructApp
{
///
/// BoxAndUnBox 的摘要說明。
///
public class BoxAndUnBox
{
public BoxAndUnBox()
{
//
// TODO: 在此處新增建構函式邏輯
//
}
/////////////////////////////////////////////////////////////////////////////////////
static void Main(string[] args)
{
double dubBox = 77.77;
object bjBox = dubBox;
double dubUnBox = (double)objBox; /// 將引用型物件拆箱 ,並返回值
Console.WriteLine("The Value is '{0}' and The UnBoxed is {1}",dubBox,dubUnBox);
}
/////////////////////////////////////////////////////////////////////////////////////
}
}

  與前面裝箱的程式碼相比,本段程式碼多加了一行double dubUnBox = (double)objBox;新加的這行程式碼作了四個工作,這個也將體現在MSIL程式碼中。第一步將一個值壓入堆疊;第二步將引用型別轉換為值型別;第三步間接將值壓棧;第四步傳值給dubUnBox。

  MSIL程式碼如下:

.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// 程式碼大小 48 (0x30)
.maxstack 3
.locals init ([0] float64 dubBox,
[1] object objBox,
[2] float64 dubUnBox)
IL_0000: ldc.r8 77.769999999999996
IL_0009: stloc.0
IL_000a: ldloc.0
IL_000b: box [mscorlib]System.Double
IL_0010: stloc.1
IL_0011: ldloc.1
IL_0012: unbox [mscorlib]System.Double
IL_0017: ldind.r8
IL_0018: stloc.2
IL_0019: ldstr "The Value is '{0}' and The UnBoxed is {1}"
IL_001e: ldloc.0
IL_001f: box [mscorlib]System.Double
IL_0024: ldloc.2
IL_0025: box [mscorlib]System.Double
IL_002a: call void [mscorlib]System.Console::WriteLine(string,
object,
object)
IL_002f: ret
} // end of method BoxAndUnBox::Main

  在MSIL中,第IL_0011 至 IL_0018 行是描述新行程式碼的。參照C#的MSIL手冊,觀者不難理解這段底層程式碼的執行過程,在此我著重描述一下objBox在拆箱時的遭遇:(1)環境須先判斷堆疊上指向合法物件的地址,以及在對此物件向指定的型別進行轉換時是否合法,如果不合法,就丟擲異常;(2)當判斷型別轉換正確,就返回一個指向物件內的值的指標。


  看來,裝箱和拆箱也不過如此,費了半天勁,剛把‘值’給裝到‘箱’裡去了,有費了更多的勁把它拆解了,鬱悶啊!細心的觀者,可能還能結合程式碼和MSIL看出,怎麼在呼叫Console.WriteLine()的過程中又出現了兩次box,是的,我本想偷懶逃過這節,但是既然已被發現,就應該大膽的面對,其實這就是傳說中的“暗箱操作”啊! 因為Console.WriteLine方法有許多的過載版本,此處的版本是以兩個String物件為引數,而具有object 型別的引數的過載是編譯器找到的最接近的版本,所以,編譯器為了求得與這個方法的原型一致,就必須對值型別的dubBox和dubUnBox分別進行裝箱(轉換成引用型別)。

  所以,為了避免由於無謂的隱式裝箱所造成的效能損失,在執行這些多型別過載方法之前,最好先對值進行裝箱。現在我們把上述地程式碼改進為:

using System;

namespace StructApp
{
///
/// BoxAndUnBox 的摘要說明。
///
public class BoxAndUnBox
{
public BoxAndUnBox()
{
//
// TODO: 在此處新增建構函式邏輯
//
}
///////////////////////////////////////////////////////////////////
static void Main(string[] args)
{
double dubBox = 77.77;
object bjBox = dubBox;
double dubUnBox = (double)objBox;
object bjUnBox = dubUnBox;
Console.WriteLine("The Value is '{0}' and The UnBoxed is {1}",objBox,objUnBox);
}
///////////////////////////////////////////////////////////////////
}
}

  MSIL程式碼:

.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// 程式碼大小 45 (0x2d)
.maxstack 3
.locals init ([0] float64 dubBox,
[1] object objBox,
[2] float64 dubUnBox,
[3] object objUnBox)
IL_0000: ldc.r8 77.769999999999996
IL_0009: stloc.0
IL_000a: ldloc.0
IL_000b: box [mscorlib]System.Double
IL_0010: stloc.1
IL_0011: ldloc.1
IL_0012: unbox [mscorlib]System.Double
IL_0017: ldind.r8
IL_0018: stloc.2
IL_0019: ldloc.2
IL_001a: box [mscorlib]System.Double
IL_001f: stloc.3
IL_0020: ldstr "The Value is '{0}' and The UnBoxed is {1}"
IL_0025: ldloc.1
IL_0026: ldloc.3
IL_0027: call void [mscorlib]System.Console::WriteLine(string,
object,
object)
IL_002c: ret
} // end of method BoxAndUnBox::Main

  我暈!這算嘛事兒呀!看完後是不是該吐血的吐血,該上吊的上吊呀!相信能堅持到看完最後一個 "!" 的同志一定是個好同志。

  其實,我們也可以妄加揣測一下:引用型應當屬於高階型別,而值型屬於原始型別,箱只是一個概念、一個秩序、一套規則或準確說是一個邏輯。原始的東西作為基礎,其複雜性和邏輯性不會很高,而高階的東西就不那麼穩定了,它會不斷的進化和發展,因為這個邏輯的‘箱’會不斷地被要求擴充和完善。由此思路推演,我們就不難預測出未來我們需要努力的方向和成功機會可能存在的地方—— !

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/12639172/viewspace-629678/,如需轉載,請註明出處,否則將追究法律責任。

相關文章