前言
之前對幾個(gè)沒什么理解,只是簡單的用過可空類型,也是知道怎么用,至于為什么,還真不太清楚,通過整理本文章學(xué)到了很多知識,也許對于以后的各種代碼優(yōu)化都有好處。
本文的重點(diǎn)就是:值類型直接存儲其值,引用類型存儲對值的引用,值類型存在堆棧上,引用類型存儲在托管堆上,值類型轉(zhuǎn)為引用類型叫做裝箱,引用類型轉(zhuǎn)為值類型叫拆箱。
這一句話概括起來很簡單,可是真正的理解起來卻沒那么簡單,對于我來說吧。
值類型和引用類型
C#值類型數(shù)據(jù)直接在他自身分配到的內(nèi)存中存儲數(shù)據(jù),而C#引用類型只是包含指向存儲數(shù)據(jù)位置的指針。
C#值類型,我們可以把他歸納成三類:
第一類:基礎(chǔ)數(shù)據(jù)類型(string類型除外):包括整型、浮點(diǎn)型、十進(jìn)制型、布爾型。
整型包括:sbyte、byte、char、short、ushort、int、uint、long、ulong 這九種類型;
浮點(diǎn)型就包括 float 和 double 兩種類型;
十進(jìn)制型就是 decimal ;
布爾型就是 bool 型了。
第二類:結(jié)構(gòu)類型:就是 struct 型
第三類:枚舉類型:就是 enum 型
C#引用類型有五種:class、interface、delegate、object、string、Array。
上面說的是怎么區(qū)分哪些C#值類型和C#引用類型,而使用上也是有區(qū)別的。所有值類型的數(shù)據(jù)都無法為null的,聲明后必須賦以初值;引用類型才允許為null。
不過這里我們可以看一下可空類型
可空類型
可空類型可以表示基礎(chǔ)類型的所有值,另外還可以表示 null 值。可空類型可通過下面兩種方式中的一種聲明:
System.Nullable<T> variableT? variable
T 是可空類型的基礎(chǔ)類型。T 可以是包括 struct 在內(nèi)的任何值類型;但不能是引用類型。
1.值類型后加問號表示此類型為可空類型,如int?i=null;
int? d = null; System.Nullable<double> e = null;
2.可空類型與一元或二元運(yùn)算符一起使用時(shí),只要有一個(gè)操作數(shù)為null,結(jié)果都為null;
3.比較可空類型時(shí),只要一個(gè)操作數(shù)為null,比較結(jié)果就為false。

值類型和引用類型在賦值(或者說復(fù)制)的時(shí)候也是有區(qū)別的。值類型數(shù)據(jù)在賦值的時(shí)候是直接復(fù)制值到新的對象中,而引用類型則只是復(fù)制對象的引用。
最后,值類型存在堆棧上,引用類型存儲在托管堆上。接下來我們來看看堆和棧吧。
棧(Stack)和堆(Heap)
Stack是指堆棧,Heap是指托管堆,在C#中的叫法應(yīng)該是這樣的。
1、堆棧stack:堆棧中存儲值類型。
堆棧實(shí)際上是自上向下填充的,即由高內(nèi)存地址指向低內(nèi)存地址填充。
堆棧的工作方式是先分配的內(nèi)存變量后釋放(先進(jìn)后出原則)。堆棧中的變量是從下向上釋放,這樣就保證了堆棧中先進(jìn)后出的規(guī)則不與變量的生命周期起沖突!
堆棧的性能非常高,但是對于所有的變量來說還不太靈活,而且變量的生命周期必須嵌套。
通常我們希望使用一種方法分配內(nèi)存來存儲數(shù)據(jù),并且方法退出后很長一段時(shí)間內(nèi)數(shù)據(jù)仍然可以使用。此時(shí)就要用到堆(托管堆)!
2、C#堆棧的工作方式
Windwos使用虛擬尋址系統(tǒng),把程序可用的內(nèi)存地址映射到硬件內(nèi)存中的實(shí)際地址,其作用是32位處理器上的每個(gè)進(jìn)程都可以使用4GB的內(nèi)存-無論計(jì)算機(jī)上有多少硬盤空間(在64位處理器上,這個(gè)數(shù)字更大些)。這4GB內(nèi)存包含了程序的所有部份-可執(zhí)行代碼,加載的DLL,所有的變量。這4GB內(nèi)存稱為虛擬內(nèi)存。
4GB的每個(gè)存儲單元都是從0開始往上排的。要訪問內(nèi)存某個(gè)空間存儲的值。就需要提供該存儲單元的數(shù)字。在高級語言中,編譯器會(huì)把我們可以理解的名稱轉(zhuǎn)換為處理器可以理解的內(nèi)存地址。
在進(jìn)程的虛擬內(nèi)存中,有一個(gè)區(qū)域稱為堆棧,用來存儲值類型。另外在調(diào)用一個(gè)方法時(shí),將使用堆棧復(fù)制傳遞給方法的所有參數(shù)。
我們來看一下下面的小例子:
public void Test() { int a; ///do something { int b; ///do something } }聲明了a之后,在內(nèi)部代碼塊中聲明了b,然后內(nèi)部代碼塊終止,b就出了作用域,然后a才出作用域。在釋放變量的時(shí)候,其順序總是與給它們分配內(nèi)存的順序相反,后進(jìn)先出,這就是堆棧的工作方式。
堆棧是向下填充的,即從高地址向低地址填充。當(dāng)數(shù)據(jù)入棧后,堆棧指針就會(huì)隨之調(diào)整,指向下一個(gè)自由空間。我們來舉個(gè)例子說明。

如圖,假如堆棧指針2000,下一個(gè)自由空間是1999。下面的代碼會(huì)告訴編譯器需要一些存儲單元來存儲一個(gè)整數(shù)和一個(gè)雙精度浮點(diǎn)數(shù)。
int c = 2; double d=3.5; ///do something
這兩個(gè)都是值類型,自然是存儲在堆棧中。聲明c賦值2后,c進(jìn)入作用域。int類型需要4個(gè)字節(jié),c就存儲在1996~1999上。此時(shí),堆棧指針就減4,指向新的已用空間的末尾1996,下一個(gè)自由空間為1995。下一行聲明d賦值3.5后,double需要占用8個(gè)字節(jié),所以存儲在1988~1995上,堆棧指針減去8。
當(dāng)d出作用域時(shí),計(jì)算機(jī)就知道這個(gè)變量已經(jīng)不需要了。變量的生存期總是嵌套的,當(dāng)d在作用域的時(shí)候,無論發(fā)生什么事情,都可以保證堆棧指針一直指向存儲d的空間。刪除這個(gè)d變量的時(shí)候堆棧指針遞增8,現(xiàn)在指向d曾經(jīng)使用過的空間,此處就是放置閉合花括號的地方。然后c也出作用域,堆棧指針再遞增4。
此時(shí)如果放入新的變量,從1999開始的存儲單元就會(huì)被覆蓋了。
3、堆(托管堆)heap堆(托管堆)存儲引用類型。
此堆非彼堆,.NET中的堆由垃圾收集器自動(dòng)管理。
與堆棧不同,堆是從下往上分配,所以自由的空間都在已用空間的上面。
4、托管堆的工作方式
堆棧有很高的性能,但要求變量的生命周期必須嵌套(后進(jìn)先出)。通常我們希望使用一個(gè)方法來分配內(nèi)存,來存儲一些數(shù)據(jù),并在方法退出后很長的一段時(shí)間內(nèi)數(shù)據(jù)仍是可用的。用new運(yùn)算符來請求空間,就存在這種可能性-例如所有引用類型。這時(shí)候就要用到托管堆了。
托管堆是進(jìn)程可用4GB的另一個(gè)區(qū)域,我們用一個(gè)例子了解托管堆的工作原理和為引用數(shù)據(jù)類型分配內(nèi)存。假設(shè)我們有一個(gè)Cat類。
public class Cat { public string Name { get; set; } }來看下面這個(gè)最簡單的方法,當(dāng)然著兩行代碼,在第一節(jié)中也有提到過http://m.survivalescaperooms.com/aehyok/p/3499822.html。
1 public void Test()2 {3 Cat cat;4 cat = new Cat();5 }第三行代碼聲明了一個(gè)Cat的引用cat,在堆棧上給這個(gè)引用分配存儲空間,但這只是一個(gè)引用,而不是實(shí)際的Cat對象。cat引用包含了存儲Cat對象的地址-需要4個(gè)字節(jié)把0~4GB之間的地址存儲為一個(gè)整數(shù)-因此cat引用占4個(gè)字節(jié)。
第四行代碼首先分配托管堆上的內(nèi)存,用來存儲Cat實(shí)例,然后把變量cat的值設(shè)置為分配給Cat對象的內(nèi)存地址。
Cat是一個(gè)引用類型,因此是放在內(nèi)存的托管堆中。為了方便討論,假設(shè)Cat對象占用32字節(jié),包括它的實(shí)例字段和.NET用于識別和管理其類實(shí)例的一些信息。為了在托管堆中找到一個(gè)存儲新Cat對象的存儲位置,.NET運(yùn)行庫會(huì)在堆中搜索一塊連續(xù)的未使用的32字節(jié)的空間,假定其起始地址是1000。而在堆棧中的內(nèi)存地址的四個(gè)字節(jié)為:1996到1999。在實(shí)例化cat之前應(yīng)該是這樣的。

cat實(shí)例化,給Cat對象分配空間之后,內(nèi)存變化為 cat在堆棧中使用1996到1999的內(nèi)存地址,然后對Cat對象分配空間之后。

這里與堆棧不同,堆上的內(nèi)存是向上分配的,所有自由空間都在已用空間的上面。
以上例子可以看出,建議引用變量的過程比建立值變量的過程復(fù)雜的多,且不能避免性能的降低-.NET運(yùn)行庫需要保持堆的信息狀態(tài),在堆添加新數(shù)據(jù)時(shí),這些信息也需要更新(這個(gè)會(huì)在堆的垃圾收集機(jī)制中提到)。盡管有這么些性能損失,但還有一種機(jī)制,在給變量分配內(nèi)存的時(shí)候,不會(huì)受到堆棧的限制:
把一個(gè)引用變量e的值賦給另一個(gè)相同類型的變量f,這兩個(gè)引用變量就都引用同一個(gè)對象了。當(dāng)變量f出作用域的時(shí)候,它會(huì)被堆棧刪除,但它所引用的對象依然保留在堆上,因?yàn)檫€有一個(gè)變量e在引用這個(gè)對象。只有該對象的數(shù)據(jù)不再被任何變量引用時(shí),它才會(huì)被刪除。
5、托管堆的垃圾收集
對象不再被引用時(shí),會(huì)刪除堆中已經(jīng)不再被引用的對象。如果僅僅是這樣,久而久之,堆上的自由空間就會(huì)分散開來,給新對象分配內(nèi)存就會(huì)很難處理,.NET運(yùn)行庫必須搜索整個(gè)堆才能找到一塊足夠大的內(nèi)存塊來存儲整個(gè)新對象。
但托管堆的垃圾收集器運(yùn)行時(shí),只要它釋放了能釋放的對象,就會(huì)壓縮其他對象,把他們都推向堆的頂部,形成一個(gè)連續(xù)的塊。在移動(dòng)對象的時(shí)候,需要更新所有對象引用的地址,會(huì)有性能損失。但使用托管堆,就只需要讀取堆指針的值,而不用搜索整個(gè)鏈接地址列表,來查找一個(gè)地方放置新數(shù)據(jù)。
因此在.NET下實(shí)例化對象要快得多,因?yàn)閷ο蠖急粔嚎s到堆的相同內(nèi)存區(qū)域,訪問對象時(shí)交換的頁面較少。Microsoft相信,盡管垃圾收集器需要做一些工作,修改它移動(dòng)的所有對象引用,導(dǎo)致性能降低,但這樣性能會(huì)得到彌補(bǔ)。
裝箱和拆箱
1、裝箱是將值類型轉(zhuǎn)換為引用類型 ;拆箱是將引用類型轉(zhuǎn)換為值類型。利用裝箱和拆箱功能,可通過允許值類型的任何值與Object 類型的值相互轉(zhuǎn)換,將值類型與引用類型鏈接起來。
例如,如下的代碼:
static void Main(string[] args) { int val = 100; object obj = val; Console.WriteLine ("對象的值 = {0}", obj); Console.ReadLine(); }這其實(shí)就是一個(gè)簡單裝箱的過程,是將值類型轉(zhuǎn)換為引用類型的過程。
static void Main(string[] args) { int val = 100; object obj = val; int num = (int)obj; Console.WriteLine("num = {0}", num); Console.ReadLine(); }接著前面裝箱的例子,那么int num=(int)obj; 這個(gè)過程就是拆箱的過程。
注意:被裝過箱的對象才能被拆箱
2、為何需要裝箱?(為何要將值類型轉(zhuǎn)為引用類型?)
一種最普通的場景是,調(diào)用一個(gè)含類型為Object的參數(shù)的方法,該Object可支持任意為型,以便通用。當(dāng)你需要將一個(gè)值類型(如Int32)傳入時(shí),需要裝箱。
另一種用法是,一個(gè)非泛型的容器,同樣是為了保證通用,而將元素類型定義為Object。于是,要將值類型數(shù)據(jù)加入容器時(shí),需要裝箱。
3、裝箱/拆箱的內(nèi)部操作。
裝箱: 對值類型在堆中分配一個(gè)對象實(shí)例,并將該值復(fù)制到新的對象中。按三步進(jìn)行。 第一步:新分配托管堆內(nèi)存(大小為值類型實(shí)例大小加上一個(gè)方法表指針和一個(gè)同步塊索引SyncBlockIndex)。 第二步:將值類型的實(shí)例字段拷貝到新分配的內(nèi)存中。 第三步:返回托管堆中新分配對象的地址。這個(gè)地址就是一個(gè)指向?qū)ο蟮囊昧恕?/p>
拆箱:
拆箱過程與裝箱過程正好相反。看一段代碼:
long a = 999999999; object b = a; int c = (int)b;
拆箱必須非常小心,確保該值變量有足夠的空間存儲拆箱后得到的值。C#int只有32位,如果把64位的long值拆箱為int時(shí),會(huì)產(chǎn)生一個(gè)InvalidCastExecption異常。
顯然,從原理上可以看出,裝箱時(shí),生成的是全新的引用對象,這會(huì)有時(shí)間損耗,也就是造成效率降低。裝箱操作和拆箱操作是要額外耗費(fèi)cpu和內(nèi)存資源的,所以在c# 2.0之后引入了泛型來減少裝箱操作和拆箱操作消耗。
4、非泛型的裝箱和拆箱以及泛型
使用非泛型集合時(shí)引發(fā)的裝箱和拆箱操作
var array = new ArrayList(); array.Add(1); array.Add(2); foreach (int value in array) { Console.WriteLine("value is {0}",value); }

代碼聲明了一個(gè)ArrayList對象,向ArrayList中添加兩個(gè)數(shù)字1,2;然后使用foreach將ArrayList中的元素打印到控制臺。
在這個(gè)過程中會(huì)發(fā)生兩次裝箱操作和兩次拆箱操作,在向ArrayList中添加int類型元素時(shí)會(huì)發(fā)生裝箱,在使用foreach枚舉ArrayList中的int類型元素時(shí)會(huì)發(fā)生拆箱操作,將object類型轉(zhuǎn)換成int類型,在執(zhí)行到Console.WriteLine時(shí),還會(huì)執(zhí)行兩次的裝箱操作;這一段代碼執(zhí)行了6次的裝箱和拆箱操作;如果ArrayList的元素個(gè)數(shù)很多,執(zhí)行裝箱拆箱的操作會(huì)更多。
使用泛型集合
var list = new List<int>();list.Add(1);list.Add(2); foreach (int value in list){Console.WriteLine("value is {0}", value);}代碼和1中的代碼的差別在于集合的類型使用了泛型的List,而非ArrayList.上述代碼只會(huì)在Console.Wri
新聞熱點(diǎn)
疑難解答
圖片精選