C#中值類型和引用類型的區(qū)別深度分析
本文通俗易懂的分析了C#中值類型和引用類型的區(qū)別。分享給大家供大家參考。具體分析如下:
似乎“值類型和引用類型的區(qū)別”是今年面試的流行趨勢(shì),我已然是連續(xù)三次(目前總共也就三次)面試第一個(gè)問題就遇到這個(gè)了,這是多大的概率啊,100%,哈哈,我該買彩票去!
言歸正傳,咱還是先來探討探討這二者之間有什么區(qū)別吧。記得有一次電話面試中,我直接跟面試官說:“值類型是現(xiàn)金,引用類型是存折”,后來想想當(dāng)時(shí)說這話雖是有點(diǎn)兒沖動(dòng)地脫口而出,但也沒什么不妥。我這人不善于背理論的教條,喜歡把書本上那些生硬的話跟現(xiàn)實(shí)生活中常見的事物聯(lián)系起來理解和記憶。
直白點(diǎn)兒說:值類型就是現(xiàn)金,要用直接用;引用類型是存折,要用還得先去銀行取現(xiàn)。
聲明一個(gè)值類型變量,編譯器會(huì)在棧上分配一個(gè)空間,這個(gè)空間對(duì)應(yīng)著該值類型變量,空間里存儲(chǔ)的就是該變量的值。引用類型的實(shí)例分配在堆上,新建一個(gè)引用類型實(shí)例,得到的變量值對(duì)應(yīng)的是該實(shí)例的內(nèi)存分配地址,這就像您的銀行賬號(hào)一樣。具體哪些類型是值類型哪些是引用類型,大家翻翻書,背一背就好了,不過我想,做過一段時(shí)間的開發(fā),即使您背不了書上教條的定義,也不會(huì)把值類型和引用類型搞混的。接下來,還是老規(guī)矩,咱看碼說話吧。
{
public string Name { get; set; }
public int Age { get; set; }
}
public static class ReferenceAndValue
{
public static void Demonstration()
{
Person zerocool = new Person { Name = "ZeroCool", Age = 25 };
Person anders = new Person { Name = "Anders", Age = 47 };
int age = zerocool.Age;
zerocool.Age = 22;
Person guru = anders;
anders.Name = "Anders Hejlsberg";
Console.WriteLine("zerocool's age:\t{0}", zerocool.Age);
Console.WriteLine("age's value:\t{0}", age);
Console.WriteLine("anders' name:\t{0}", anders.Name);
Console.WriteLine("guru' name:\t{0}", guru.Name);
}
}
上面這段代碼,我們首先創(chuàng)建了一個(gè)Person類,包含了Name和Age兩個(gè)屬性,毋庸置疑,Person類是引用類型,Name也是,因?yàn)樗莝tring類型的(但string是很特殊的引用類型,后面將專門有一篇文章來討論),但Age則是值類型。接下來我們來看看Demonstration方法,其中演示的就是值類型跟引用類型的區(qū)別。
首先,我們聲明了兩個(gè)Person類的實(shí)例對(duì)象,zerocool和anders,前面提到過,這兩個(gè)對(duì)象都被分配在堆上,而zerocool和anders本身其實(shí)只是對(duì)象所在內(nèi)存區(qū)域的起始地址引用,換句話說就是指向這里的指針。我們聲明對(duì)象實(shí)例時(shí)也順便分別進(jìn)行了初始化,首先我們看,zerocool對(duì)象的值類型成員,我們賦值為25(對(duì),我今年25歲),anders(待會(huì)兒你們就知道是誰了)的Name屬性,我們賦值為“Anders”。齊活兒,接下來看我們?cè)趺锤砂伞?/p>
我們聲明一個(gè)值類型變量age,直接在初始化時(shí)把zerocool的Age值賦給它,顯然,age的值就是25了。但這個(gè)時(shí)候zerocool不高興了,他想裝嫩,私自把自己的年齡改成22歲,剛夠法定結(jié)婚年齡。然后我們又聲明了一個(gè)引用類型的guy對(duì)象,初始化時(shí)就把a(bǔ)nders賦給它,然后anders露出廬山真面目了,他的名字叫“Anders Hejlsberg”(在此向C#之父致敬)。接下來我們來分別答應(yīng)出這幾個(gè)變量的值,看看有什么差別。
Result01
你可能要覺得奇怪(你要不覺得奇怪,也就不用再接著往下看了),為什么我們改了zerocool.Age的值,age沒跟著變,改了anders.Name的值,guru.Name卻跟著變了呢?這就是值類型和引用類型的區(qū)別。我們聲明age值類型變量,并將zerocool.Age賦給它,編譯器在棧上分配了一塊空間,然后把zerocool.Age的值填進(jìn)去,僅此而已,二者并無任何牽連,就像復(fù)印機(jī)一樣,只是把zerocool.Age的值拷貝給age了。而引用類型不一樣,我們?cè)诼暶鱣uy的時(shí)候把a(bǔ)nders賦給它,前面說過,引用類型包含的是只想堆上數(shù)據(jù)區(qū)域地址的引用,其實(shí)就是把a(bǔ)nders的引用也賦給guy了,因此這二者從此指向了同一塊內(nèi)存區(qū)域,既然是指向同一塊區(qū)域,那么甭管誰動(dòng)了里面的“奶酪”,另一個(gè)變現(xiàn)出來的結(jié)果也會(huì)跟著變,就像信用卡跟親情卡一樣,用親情卡取了錢,與之關(guān)聯(lián)的信用卡賬上也會(huì)跟著發(fā)生變化。一提到錢,估計(jì)大家伙兒印象就深了些吧,呵呵!
另外,性能上也會(huì)有區(qū)別的。既然一個(gè)是直接操作內(nèi)存,另一個(gè)則多一步先解析引用地址,那么顯然很多時(shí)候值類型會(huì)減小系統(tǒng)性能開銷。但“很多時(shí)候”不代表“所有時(shí)候”,有些時(shí)候還得量力而為,例如需要大量進(jìn)行函數(shù)參數(shù)傳遞或返回的時(shí)候,老是這樣進(jìn)行字段拷貝,其實(shí)反而會(huì)降低應(yīng)用程序性能。另外,如果實(shí)例會(huì)被頻繁地用于Hashtable或者ArrayList之類的集合中,這些類會(huì)對(duì)其中的值類型變量進(jìn)行裝箱操作,這也會(huì)導(dǎo)致額外的內(nèi)存分配和內(nèi)存拷貝操作,從應(yīng)用程序性能方面來看,其實(shí)也不劃算。
哦對(duì)了,上面提到了一個(gè)概念,裝箱。那么什么是裝箱呢?其實(shí)裝箱就是值類型到引用類型的轉(zhuǎn)化過程。將一個(gè)值類型變量裝箱成一個(gè)引用類型變量,首先會(huì)在托管堆上為新的引用類型變量分配內(nèi)存空間,然后將值類型變量拷貝到托管堆上新分配的對(duì)象內(nèi)存中,最后返回新分配的對(duì)象內(nèi)存地址。裝箱操作是可逆的,所以還有拆箱操作。拆箱操作獲取指向?qū)ο笾邪殿愋筒糠值闹羔?,然后由程序員手動(dòng)將其對(duì)應(yīng)的值拷貝給值類型變量。接下來我們來看看典型的裝箱和拆箱操作。
{
public static void Demonstration()
{
int ageInt = new int();
// Boxing operation.
object ageObject = ageInt;
//ageObject = null;
// Unboxing operation.
ageInt = (int)ageObject;
Console.WriteLine(ageInt);
}
}
在該方法中,我們首先聲明了一個(gè)值類型變量ageInt,但并未給它賦值,接著聲明了一個(gè)典型的引用類型變量ageObject,并把a(bǔ)geInt賦給它,這里就進(jìn)行了一次裝箱操作。編譯器現(xiàn)在托管堆上分配一塊內(nèi)存空間(空間大小為對(duì)象中包含的值類型變量所占空間總和外加一個(gè)方法表指針和一個(gè)SyncBlockIndex),然后把a(bǔ)geInt拷貝到這個(gè)空間中,再返回該空間的引用地址。接下來第13行則是拆箱操作,編譯器獲取到ageObject對(duì)象中值類型變量的指針,然后將其值拷貝給值類型變量。如果你把第10行注釋掉的代碼打開(這是通俗說法,其實(shí)就是取消注釋),那么第13行就會(huì)拋出System.NullReferenceException異常,要說問什么,這又會(huì)牽扯出值類型跟引用類型另一個(gè)大的不同??匆娏税桑暶鱝geInt時(shí)并沒有賦值,如果關(guān)掉第10行代碼,程序不會(huì)報(bào)錯(cuò),最后打印出個(gè)0,這說明在聲明值類型變量時(shí),如果沒有初始化賦值,編譯器會(huì)自動(dòng)將其賦值為0,既然值類型沒有引用,那么它就不可能為空。引用類型不一樣,它可以為空引用,一張過期作廢的銀行卡是可以存在。而如果將一個(gè)空的對(duì)象拆箱,編譯器上哪兒去找它里面的值類型變量的指針呢?所以這也是拆箱操作需要注意的地方。
最后,我們?cè)诎阎殿愋秃鸵妙愋椭g其它一些明顯區(qū)別大致羅列如下,以便大家能順利通過面試第一問。
所有值類型都繼承自System.ValueType,但是ValueType沒有附加System.Object包含之外其它任何方法,不過它倒是改寫了Equals和GetHashCode兩個(gè)方法。引用類型變量的Equals比較的是二者的引用地址而不是內(nèi)部的值,值類型變量的Equals方法比較的是二者的值而不是……哦對(duì)了,值類型壓根兒沒有引用地址;
值類型不能作為其它任何類型的基類型,因此不能向值類型中增加任何新的虛方法,更不該有任何抽象方法,所有的方法都是sealed的(不可重寫);
未裝箱的值類型分配在棧上而不是堆上,而棧又不是GC的地盤兒,因此GC根本不過問值類型變量的死活,一旦值類型變量的作用范圍一過,它所占的內(nèi)存空間就立即被回收掉,不勞GC親自動(dòng)手。
以上羅列的都是值類型和引用類型之間的主要區(qū)別,文碼并茂,相信應(yīng)該給你留下比較深刻的印象,雖不夠深,但愿能起到拋磚引玉的作用。如果去面SDE職位,估計(jì)這深度就差不多了。
希望本文所述對(duì)大家的C#程序設(shè)計(jì)有所幫助。
相關(guān)文章
通過容器擴(kuò)展屬性IExtenderProvider實(shí)現(xiàn)WinForm通用數(shù)據(jù)驗(yàn)證組件
這篇文章介紹了通過容器擴(kuò)展屬性IExtenderProvider實(shí)現(xiàn)WinForm通用數(shù)據(jù)驗(yàn)證組件的方法,文中通過示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-12-12
C#實(shí)現(xiàn)泛型動(dòng)態(tài)循環(huán)數(shù)組隊(duì)列的方法
隊(duì)列一種先進(jìn)先出的數(shù)據(jù)結(jié)構(gòu),本文通過實(shí)例代碼給大家介紹下C#實(shí)現(xiàn)泛型動(dòng)態(tài)循環(huán)數(shù)組隊(duì)列的方法,感興趣的朋友一起看看吧2022-01-01
C#實(shí)現(xiàn)萬物皆可排序的隊(duì)列方法詳解
本文詳細(xì)講解了C#實(shí)現(xiàn)萬物皆可排序隊(duì)列的方法,文中通過示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-07-07
WPF利用TextBlock實(shí)現(xiàn)查找結(jié)果高亮顯示效果
在應(yīng)用開發(fā)過程中,經(jīng)常遇到這樣的需求:通過關(guān)鍵字查找數(shù)據(jù),把帶有關(guān)鍵字的數(shù)據(jù)顯示出來,同時(shí)在結(jié)果中高亮顯示關(guān)鍵字,所以本文就來和大家介紹一下如何利用TextBlock實(shí)現(xiàn)查找結(jié)果高亮顯示效果吧2023-08-08

