前言
之前對幾個沒什麼了解,隻是簡單的用過可空類型,也是知道怎麼用,至于為什麼,還真不太清楚,通過整理本文章學到了很多知識,也許對于以後的各種代碼優化都有好處。
本文的重點就是:值類型直接存儲其值,引用類型存儲對值的引用,值類型存在堆棧上,引用類型存儲在托管堆上,值類型轉為引用類型叫做裝箱,引用類型轉為值類型叫拆箱。
這一句話概括起來很簡單,可是真正的了解起來卻沒那麼簡單,對于我來說吧。
值類型和引用類型
C#值類型資料直接在他自身配置設定到的記憶體中存儲資料,而C#引用類型隻是包含指向存儲資料位置的指針。
C#值類型,我們可以把他歸納成三類:
第一類: 基礎資料類型(string類型除外):包括整型、浮點型、十進制型、布爾型。
整型包括:sbyte、byte、char、short、ushort、int、uint、long、ulong 這九種類型;
浮點型就包括 float 和 double 兩種類型;
十進制型就是 decimal ;
布爾型就是 bool 型了。
第二類:結構類型:就是 struct 型
第三類:枚舉類型:就是 enum 型
C#引用類型有五種:class、interface、delegate、object、string、Array。
上面說的是怎麼區分哪些C#值類型和C#引用類型,而使用上也是有差別的。所有值類型的資料都無法為null的,聲明後必須賦以初值;引用類型才允許為null。
不過這裡我們可以看一下可空類型
可空類型
可空類型可以表示基礎類型的所有值,另外還可以表示 null 值。可空類型可通過下面兩種方式中的一種聲明:
T 是可空類型的基礎類型。T 可以是包括 struct 在内的任何值類型;但不能是引用類型。
1.值類型後加問号表示此類型為可空類型,如int? i = null;
2.可空類型與一進制或二進制運算符一起使用時,隻要有一個操作數為null,結果都為null;

3.比較可空類型時,隻要一個操作數為null,比較結果就為false。
值類型和引用類型在指派(或者說複制)的時候也是有差別的。值類型資料在指派的時候是直接複制值到新的對象中,而引用類型則隻是複制對象的引用。
最後,值類型存在堆棧上,引用類型存儲在托管堆上。接下來我們來看看堆和棧吧。
棧(Stack)和堆(Heap)
Stack是指堆棧,Heap是指托管堆,在C#中的叫法應該是這樣的。
1、堆棧stack:堆棧中存儲值類型。
堆棧實際上是自上向下填充的,即由高記憶體位址指向低記憶體位址填充。
堆棧的工作方式是先配置設定的記憶體變量後釋放(先進後出原則)。堆棧中的變量是從下向上釋放,這樣就保證了堆棧中先進後出的規則不與變量的生命周期起沖突!
堆棧的性能非常高,但是對于所有的變量來說還不太靈活,而且變量的生命周期必須嵌套。
通常我們希望使用一種方法配置設定記憶體來存儲資料,并且方法退出後很長一段時間内資料仍然可以使用。此時就要用到堆(托管堆)!
2、C#堆棧的工作方式
Windwos使用虛拟尋址系統,把程式可用的記憶體位址映射到硬體記憶體中的實際位址,其作用是32位處理器上的每個程序都可以使用4GB的記憶體-無論計算機上有多少硬碟空間(在64位處理器上,這個數字更大些)。這4GB記憶體包含了程式的所有部份-可執行代碼,加載的DLL,所有的變量。這4GB記憶體稱為虛拟記憶體。
4GB的每個存儲單元都是從0開始往上排的。要通路記憶體某個空間存儲的值。就需要提供該存儲單元的數字。在進階語言中,編譯器會把我們可以了解的名稱轉換為處理器可以了解的記憶體位址。
在程序的虛拟記憶體中,有一個區域稱為堆棧,用來存儲值類型。另外在調用一個方法時,将使用堆棧複制傳遞給方法的所有參數。
我們來看一下下面的小例子:
聲明了a之後,在内部代碼塊中聲明了b,然後内部代碼塊終止,b就出了作用域,然後a才出作用域。在釋放變量的時候,其順序總是與給它們配置設定記憶體的順序相反,後進先出,這就是堆棧的工作方式。
堆棧是向下填充的,即從高位址向低位址填充。當資料入棧後,堆棧指針就會随之調整,指向下一個自由空間。我們來舉個例子說明。
如圖,假如堆棧指針2000,下一個自由空間是1999。下面的代碼會告訴編譯器需要一些存儲單元來存儲一個整數和一個雙精度浮點數。
這兩個都是值類型,自然是存儲在堆棧中。聲明c指派2後,c進入作用域。int類型需要4個位元組,c就存儲在1996~1999上。此時,堆棧指針就減4,指向新的已用空間的末尾1996,下一個自由空間為1995。下一行聲明d指派3.5後,double需要占用8個位元組,是以存儲在1988~1995上,堆棧指針減去8。
當d出作用域時,計算機就知道這個變量已經不需要了。變量的生存期總是嵌套的,當d在作用域的時候,無論發生什麼事情,都可以保證堆棧指針一直指向存儲d的空間。删除這個d變量的時候堆棧指針遞增8,現在指向d曾經使用過的空間,此處就是放置閉合花括号的地方。然後c也出作用域,堆棧指針再遞增4。
此時如果放入新的變量,從1999開始的存儲單元就會被覆寫了。
3、堆(托管堆)heap堆(托管堆)存儲引用類型。
此堆非彼堆,.NET中的堆由垃圾收集器自動管理。
與堆棧不同,堆是從下往上配置設定,是以自由的空間都在已用空間的上面。
4、托管堆的工作方式
堆棧有很高的性能,但要求變量的生命周期必須嵌套(後進先出)。通常我們希望使用一個方法來配置設定記憶體,來存儲一些資料,并在方法退出後很長的一段時間内資料仍是可用的。用new運算符來請求空間,就存在這種可能性-例如所有引用類型。這時候就要用到托管堆了。
托管堆是程序可用4GB的另一個區域,我們用一個例子了解托管堆的工作原理和為引用資料類型配置設定記憶體。假設我們有一個Cat類。
第三行代碼聲明了一個Cat的引用cat,在堆棧上給這個引用配置設定存儲空間,但這隻是一個引用,而不是實際的Cat對象。cat引用包含了存儲Cat對象的位址-需要4個位元組把0~4GB之間的位址存儲為一個整數-是以cat引用占4個位元組。
第四行代碼首先配置設定托管堆上的記憶體,用來存儲Cat執行個體,然後把變量cat的值設定為配置設定給Cat對象的記憶體位址。
Cat是一個引用類型,是以是放在記憶體的托管堆中。為了友善讨論,假設Cat對象占用32位元組,包括它的執行個體字段和.NET用于識别和管理其類執行個體的一些資訊。為了在托管堆中找到一個存儲新Cat對象的存儲位置,.NET運作庫會在堆中搜尋一塊連續的未使用的32位元組的空間,假定其起始位址是1000。而在堆棧中的記憶體位址的四個位元組為:1996到1999。在執行個體化cat之前應該是這樣的。
cat執行個體化,給Cat對象配置設定空間之後,記憶體變化為 cat在堆棧中使用1996到1999的記憶體位址,然後對Cat對象配置設定空間之後。
這裡與堆棧不同,堆上的記憶體是向上配置設定的,所有自由空間都在已用空間的上面。
以上例子可以看出,建議引用變量的過程比建立值變量的過程複雜的多,且不能避免性能的降低-.NET運作庫需要保持堆的資訊狀态,在堆添加新資料時,這些資訊也需要更新(這個會在堆的垃圾收集機制中提到)。盡管有這麼些性能損失,但還有一種機制,在給變量配置設定記憶體的時候,不會受到堆棧的限制:
把一個引用變量e的值賦給另一個相同類型的變量f,這兩個引用變量就都引用同一個對象了。當變量f出作用域的時候,它會被堆棧删除,但它所引用的對象依然保留在堆上,因為還有一個變量e在引用這個對象。隻有該對象的資料不再被任何變量引用時,它才會被删除。
5、托管堆的垃圾收集
對象不再被引用時,會删除堆中已經不再被引用的對象。如果僅僅是這樣,久而久之,堆上的自由空間就會分散開來,給新對象配置設定記憶體就會很難處理,.NET運作庫必須搜尋整個堆才能找到一塊足夠大的記憶體塊來存儲整個新對象。
但托管堆的垃圾收集器運作時,隻要它釋放了能釋放的對象,就會壓縮其他對象,把他們都推向堆的頂部,形成一個連續的塊。在移動對象的時候,需要更新所有對象引用的位址,會有性能損失。但使用托管堆,就隻需要讀取堆指針的值,而不用搜尋整個連結位址清單,來查找一個地方放置新資料。
是以在.NET下執行個體化對象要快得多,因為對象都被壓縮到堆的相同記憶體區域,通路對象時交換的頁面較少。Microsoft相信,盡管垃圾收集器需要做一些工作,修改它移動的所有對象引用,導緻性能降低,但這樣性能會得到彌補。
裝箱和拆箱
1、裝箱是将值類型轉換為引用類型 ;拆箱是将引用類型轉換為值類型。
利用裝箱和拆箱功能,可通過允許值類型的任何值與Object 類型的值互相轉換,将值類型與引用類型連結起來。
例如,如下的代碼:
這其實就是一個簡單裝箱的過程,是将值類型轉換為引用類型的過程。
接着前面裝箱的例子,那麼int num=(int)obj; 這個過程就是拆箱的過程。
注意:被裝過箱的對象才能被拆箱
2、為何需要裝箱?(為何要将值類型轉為引用類型?)
一種最普通的場景是,調用一個含類型為Object的參數的方法,該Object可支援任意為型,以便通用。當你需要将一個值類型(如Int32)傳入時,需要裝箱。
另一種用法是,一個非泛型的容器,同樣是為了保證通用,而将元素類型定義為Object。于是,要将值類型資料加入容器時,需要裝箱。
3、裝箱/拆箱的内部操作。
裝箱:
對值類型在堆中配置設定一個對象執行個體,并将該值複制到新的對象中。按三步進行。
第一步:新配置設定托管堆記憶體(大小為值類型執行個體大小加上一個方法表指針和一個同步塊索引SyncBlockIndex)。
第二步:将值類型的執行個體字段拷貝到新配置設定的記憶體中。
第三步:傳回托管堆中新配置設定對象的位址。這個位址就是一個指向對象的引用了。
拆箱:
拆箱過程與裝箱過程正好相反。看一段代碼:
拆箱必須非常小心,確定該值變量有足夠的空間存儲拆箱後得到的值。C#int隻有32位,如果把64位的long值拆箱為int時,會産生一個InvalidCastExecption異常。
顯然,從原理上可以看出,裝箱時,生成的是全新的引用對象,這會有時間損耗,也就是造成效率降低。裝箱操作和拆箱操作是要額外耗費cpu和記憶體資源的,是以在c# 2.0之後引入了泛型來減少裝箱操作和拆箱操作消耗。
4、非泛型的裝箱和拆箱以及泛型
使用非泛型集合時引發的裝箱和拆箱操作
代碼聲明了一個ArrayList對象,向ArrayList中添加兩個數字1,2;然後使用foreach将ArrayList中的元素列印到控制台。
在這個過程中會發生兩次裝箱操作和兩次拆箱操作,在向ArrayList中添加int類型元素時會發生裝箱,在使用foreach枚舉ArrayList中的int類型元素時會發生拆箱操作,将object類型轉換成int類型,在執行到Console.WriteLine時,還會執行兩次的裝箱操作;這一段代碼執行了6次的裝箱和拆箱操作;如果ArrayList的元素個數很多,執行裝箱拆箱的操作會更多。
使用泛型集合
代碼和1中的代碼的差别在于集合的類型使用了泛型的List,而非ArrayList.上述代碼隻會在Console.WriteLine()方法時執行2次裝箱操作,不需要拆箱操作。
可以看出泛型可以避免裝箱拆箱帶來的不必要的性能消耗;當然泛型的好處不止于此,泛型還可以增加程式的可讀性,使程式更容易被複用等等,至于泛型以後再做詳細介紹。
總結
趕腳自己還是學了不少東西的,沒事的時候多拿出來看看,說不定還會有意想不到的收獲呢,繼續加油!