程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#基礎知識 >> 關於Visual C#裝箱與拆箱地研究

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

編輯:C#基礎知識
  在對這個問題展開討論之前,我們不妨先來問這麼幾個問題,以系統的了解我們今天要探究的主題。

  觀者也許曾無數次的使用過諸如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
  我暈!這算嘛事兒呀!看完後是不是該吐血的吐血,該上吊的上吊呀!相信能堅持到看完最後一個 "!" 的同志一定是個好同志。

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


  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved