C# 裝箱 和 拆箱 研究 — 我認為最好的

來源:互聯網
上載者:User

    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代碼察看工具)為此我們先來看看以下的代碼:

  (注釋下: Visual studio 2010的IL DASM 在Visual Studio 2010的Microsoft Windows SDK Tools下面。)

using System;using System.Collections.Generic;using System.Linq;using System.Text;namespace BoxAndUnbox{    class Program    {        static void Main(string[] args)        {            // Define a value            double dubBox = 77.77;            // Box the value of the variable to a reference            object objBox = dubBox;            Console.WriteLine("The value is '{0}' and the Boxed is {1}", dubBox, objBox.ToString());        }    }}

 

查看IL DASM代碼

.method private hidebysig static void  Main(string[] args) cil managed{  .entrypoint  // Code size       42 (0x2a)  .maxstack  3  .locals init ([0] float64 dubBox,           [1] object objBox)  IL_0000:  nop  IL_0001:  ldc.r8     77.769999999999996  IL_000a:  stloc.0  IL_000b:  ldloc.0  IL_000c:  box        [mscorlib]System.Double  IL_0011:  stloc.1  IL_0012:  ldstr      "The value is '{0}' and the Boxed is {1}"  IL_0017:  ldloc.0  IL_0018:  box        [mscorlib]System.Double  IL_001d:  ldloc.1  IL_001e:  callvirt   instance string [mscorlib]System.Object::ToString()  IL_0023:  call       void [mscorlib]System.Console::WriteLine(string,                                                                object,                                                                object)  IL_0028:  nop  IL_0029:  ret} // end of method Program::Main

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

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

using System;using System.Collections.Generic;using System.Linq;using System.Text;namespace BoxAndUnbox{    class Program    {        static void Main(string[] args)        {            // Define a value            double dubBox = 77.77;            // Box the value of the variable to a reference            object objBox = dubBox;            // Unbox            double dubUnBox = (double)objBox;            Console.WriteLine("The value is '{0}' and the Boxed is {1}", dubBox, objBox.ToString());        }    }}

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

 

再看看IL DASM代碼

.method private hidebysig static void  Main(string[] args) cil managed{  .entrypoint  // Code size       49 (0x31)  .maxstack  3  .locals init ([0] float64 dubBox,           [1] object objBox,           [2] float64 dubUnBox)  IL_0000:  nop  IL_0001:  ldc.r8     77.769999999999996  IL_000a:  stloc.0  IL_000b:  ldloc.0  IL_000c:  box        [mscorlib]System.Double  IL_0011:  stloc.1  IL_0012:  ldloc.1  IL_0013:  unbox.any  [mscorlib]System.Double  IL_0018:  stloc.2  IL_0019:  ldstr      "The value is '{0}' and the Boxed is {1}"  IL_001e:  ldloc.0  IL_001f:  box        [mscorlib]System.Double  IL_0024:  ldloc.1  IL_0025:  callvirt   instance string [mscorlib]System.Object::ToString()  IL_002a:  call       void [mscorlib]System.Console::WriteLine(string,                                                                object,                                                                object)  IL_002f:  nop  IL_0030:  ret} // end of method Program::Main

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

 

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

 

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

using System;using System.Collections.Generic;using System.Linq;using System.Text;namespace BoxAndUnbox{    class Program    {        static void Main(string[] args)        {            // Define a value            double dubBox = 77.77;            // Box the value of the variable to a reference            object objBox = dubBox;            // Unbox            double dubUnBox = (double)objBox;            object objUnBox = dubUnBox;            Console.WriteLine("The value is '{0}' and the Boxed is {1}", objBox, objUnBox);        }    }}

 

再來看看IL DASM代碼

.method private hidebysig static void  Main(string[] args) cil managed{  .entrypoint  // Code size       46 (0x2e)  .maxstack  3  .locals init ([0] float64 dubBox,           [1] object objBox,           [2] float64 dubUnBox,           [3] object objUnBox)  IL_0000:  nop  IL_0001:  ldc.r8     77.769999999999996  IL_000a:  stloc.0  IL_000b:  ldloc.0  IL_000c:  box        [mscorlib]System.Double  IL_0011:  stloc.1  IL_0012:  ldloc.1  IL_0013:  unbox.any  [mscorlib]System.Double  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 Boxed is {1}"  IL_0025:  ldloc.1  IL_0026:  ldloc.3  IL_0027:  call       void [mscorlib]System.Console::WriteLine(string,                                                                object,                                                                object)  IL_002c:  nop  IL_002d:  ret} // end of method Program::Main

 

這裡就沒有BOX的過程了。

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

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

最後說說

裝箱/拆箱對執行效率的影響
顯然,從原理上可以看出,裝箱時,產生的是全新的引用對象,這會有時間損耗,也就是造成效率降低。
那該如何做呢?
首先,應該盡量避免裝箱。
比如上例2的兩種情況,都可以避免,在第一種情況下,可以通過重載函數來避免。第二種情況,則可以通過泛型來避免。
當然,凡事並不能絕對,假設你想改造的代碼為第三方程式集,你無法更改,那你只能是裝箱了。
對於裝箱/拆箱代碼的最佳化,由於C#中對裝箱和拆箱都是隱式的,所以,根本的方法是對代碼進行分析,而分析最直接的方式是瞭解原理結何查看反編譯的IL代碼。比如:在迴圈體中可能存在多餘的裝箱,你可以簡單採用提前裝箱方式進行最佳化。

 

通過泛型來避免這個在泛型程式設計裡就講到O(∩_∩)O哈! 

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.