關於Visual C#裝箱與拆箱的研究

來源:互聯網
上載者:User
visual 關於Visual C#裝箱與拆箱的研究
2004-09-15 作者: 出處: CSDN

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

  觀者也許曾無數次的使用過諸如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 objBox = 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 objBox = 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 objBox = dubBox;
double dubUnBox = (double)objBox;
object objUnBox = 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

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

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




相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.