內容導讀
•概述
•當你聲明一個變數背後發生了什麼?
•堆疊和堆疊
•值型別和參考型別
•哪些是值型,哪些是參考型別?
•裝箱與拆箱
•裝箱與拆箱的效能問題
一、概述
本文會闡述六個重要的概念:堆疊、堆疊、值型、引用型、裝箱與拆箱箱。本文首先會透過闡述當你定義一個變數之後系統內部發生的改變開始講解,然後將關注點轉移到儲存雙雄:堆和棧。之後,我們會探討一下值類型和引用類型,並對有關這兩種類型的重要基礎內容做一個講解。
本文會透過一個簡單的程式碼來展示在裝箱和拆箱過程中所帶來的效能上的影響,請各位仔細閱讀。
二、當你聲明一個變數背後發生了什麼事?
當你在一個.NET應用程式中定義一個變數時,在RAM中會為其分配一些記憶體區塊。這塊記憶體有三樣東西:變數的名稱、變數的資料型態、變數的值。
上面簡單闡述了記憶體中發生的事情,但是你的變數究竟會被分配到哪種類型的記憶體取決於資料型別。在.NET中有兩種可分配的記憶體:棧和堆。在接下來的幾個部分中,我們會試著詳細地來理解這兩種類型的儲存。
三、存儲雙雄:堆和
為了理解以下堆棧,讓我們透過理解到底發生了什麼。
public void Method1() { // Line 1 int i=4; // Line 2 int y=2; //Line 3 class1 cls1 = new class1(); }
程式碼只有三行,現在我們可以一行一行地來了解到底內部是怎麼來執行的。
•Line 1:當這一行被執行後,編譯器會在堆疊上分配一小塊記憶體。棧會在負責追蹤你的應用程式中是否有運行記憶體需要
•Line 2:現在將會執行第二步。正如堆疊的名字一樣,它會將此處的一小塊記憶體分配疊加在剛剛第一步的記憶體分配的頂部。你可以認為棧就是一個一個疊加的房間或盒子。在堆疊中,資料的分配和解除都會透過LIFO (Last In First Out)即先進後出的邏輯規則進行。換句話說,也就是最早進入棧中的資料項有可能最後才會出棧。
•Line 3:在第三行中,我們建立了一個物件。當這一行被執行後,.NET會在堆疊中建立一個指針,而實際的物件將會儲存到一個叫做「堆」的記憶體區域。 「堆」不會監測運行內存,它只是能夠被隨時訪問到的一堆對象而已。不同於棧,堆用於動態記憶體的分配。
•這裡需要注意的另一個重要的點是物件的引用指標是分配在堆疊上的。 例如:聲明語句 Class1 cls1; 其實並沒有為Class1的實例分配內存,它只是在棧上為變量cls1創建了一個引用指針(並且將其默認職位null)。只有當其遇到new關鍵字時,它才會在堆上為物件分配記憶體。
•離開這個Method1方法時(the fun):現在執行控制語句開始離開方法體,這時所有在堆疊上為變數所分配的記憶體空間都會被清除。換句話說,在上面的範例中所有與int型別相關的變數將會按照「LIFO」後進先出的方式從堆疊中一個一個出棧。
•要注意的是:這時它並不會釋放堆中的記憶體區塊,堆中的記憶體區塊將會由垃圾回收器稍候進行清理。
現在我們許多的開發者朋友一定很好奇為什麼會有兩種不同類型的儲存?我們為什麼不能將所有的記憶體區塊分配只到一種類型的儲存?
如果你觀察足夠仔細,基元資料型別並不復雜,他們只是保存像 ‘int i = 0’這樣的值。物件資料型別就複雜了,他們引用其他物件或其他基元資料型別。換句話說,他們保存其他多個值的引用並且這些值必須一一地儲存在記憶體中。物件類型需要的是動態記憶體而基元類型需要靜態記憶體。如果需求是動態記憶體的話,那麼它將會在堆上為其分配內存,相反,則會在堆疊上為其分配。
四、值類型和引用類型
既然我們已經了解了棧和堆的概念了,是時候了解值類型和引用類型的概念了。值型別將資料和記憶體都保存在同一位置,而一個參考型別則會有一個指向實際記憶體區域的指標。
透過下圖,我們可以看到一個名為i的整形資料類型,它的值被賦值到另一個名為j的整形資料類型。他們的值都被儲存到了堆疊上。
當我們將一個int類型的值賦值到另一個int類型的值時,它實際上是創建了一個完全不同的副本。換句話說,如果你改變了其中某一個的值,另一個不會改變。於是,這些種類的資料型別被稱為「值型別」。
當我們建立一個物件並且將此物件賦值給另外一個物件時,他們彼此都指向瞭如下圖程式碼段所示的記憶體中同一塊區域。因此,當我們將obj賦值給obj1時,他們都指向了堆中的同一塊區域。換句話說,如果此時我們改變了其中任何一個,另一個都會受到影響,這也說明了他們為何被稱為「引用類型」。
五、哪些是值型,哪些是引用型?
在.NET中,變數是儲存到堆疊還是堆疊中完全取決於其所屬的資料類型。例如:‘String’或‘Object’屬於引用類型,而其他.NET基元資料類型則會被分配到堆疊上。下圖則詳細地展示了在.NET預置類型中,哪些是值類型,哪些又是參考類型。
六、裝箱和拆箱
現在,你已經有了不少的理論基礎了。現在,是時候了解上面的知識在實際程式設計中的使用了。在應用中最大的意義就在於:理解資料從堆疊移動到堆疊的過程中所發生的效能消耗問題,反之亦然。
考慮一下以下的程式碼片段,當我們將一個值類型轉換為參考類型,資料將會從堆疊移到堆中。相反,當我們將一個引用類型轉換為值類型時,資料也會從堆疊移動到堆疊中。
不管是在從堆疊移動到堆疊還是從堆中移動到堆疊上都會不可避免地對系統效能產生一些影響。
於是,兩個新名詞橫空出世:當資料從值型別轉換為引用型別的過程稱為“裝箱”,而從參考型別轉換為值型別的過程則被變成“拆箱”。
如果你編譯一下上面這段程式碼並且在ILDASM(一個IL的反編譯工具)中對其進行查看,你會發現在IL程式碼中,裝箱和拆箱是什麼樣子的。下圖則展示了範例程式碼被編譯後所產生的IL程式碼。
七、裝箱和拆箱的性能問題
雖然以上程式碼段沒有顯示拆箱操作,但其效果同樣適用於拆箱。你可以透過寫程式碼來實現拆箱,並且透過Stopwatch來測試其時間消耗。 以上就是.NET中的六個重要概念:棧、堆、值類型、引用類型、裝箱和拆箱 的內容,更多相關內容請關注PHP中文網(m.sbmmt.com)!
為了弄清楚的裝箱和拆箱會帶來怎樣的性能影響,我們分別循環運行10000次下圖所示的00次兩個函數方法。其中第一個方法中有裝箱操作,另一個則沒有。我們使用一個Stopwatch物件來監視時間的消耗。
具有裝箱操作的方法花費了3542毫秒來執行完成,而沒有裝箱操作的方法只花費了2477毫秒,整整相差了1秒多。而且,這個數值也會因為循環次數的增加而增加。也就是說,我們要盡量避免裝箱和拆箱操作。在一個專案中,如果你需要裝箱和裝箱,請仔細考慮它是否是絕對必不可少的操作,如果不是,那麼盡量不用。