值類型和引用類型的區別(轉)

似乎“值類型和引用類型的區別”是今年面試的流行趨勢,我已然是連續三次(目前總共也就三次)面試第一個問題就遇到這個了,這是多大的概率啊,100%,哈哈,我該買彩票去!

言歸正傳,咱還是先來探討探討這二者之間有什麼區別吧。記得有一次電話面試中,我直接跟面試官說:“值類型是現金,引用類型是存摺”,後來想想當時說這話雖是有點兒衝動地脫口而出,但也沒什麼不妥。我這人不善於背理論的教條,喜歡把書本上那些生硬的話跟現實生活中常見的事物聯繫起來理解和記憶。

直白點兒說:值類型就是現金,要用直接用;引用類型是存摺,要用還得先去銀行取現。

聲明一個值類型變量,編譯器會在棧上分配一個空間,這個空間對應着該值類型變量,空間裏存儲的就是該變量的值。引用類型的實例分配在堆上,新建一個引用類型實例,得到的變量值對應的是該實例的內存分配地址,這就像您的銀行賬號一樣。具體哪些類型是值類型哪些是引用類型,大家翻翻書,背一背就好了,不過我想,做過一段時間的開發,即使您背不了書上教條的定義,也不會把值類型和引用類型搞混的。接下來,還是老規矩,咱看碼說話吧。

   1: public class Person
   2: {
   3:     public string Name { get; set; }
   4:     public int Age { get; set; }
   5: }
   6:  
   7: public static class ReferenceAndValue
   8: {
   9:     public static void Demonstration()
  10:     {
  11:         Person zerocool = new Person { Name = "ZeroCool", Age = 25 };
  12:         Person anders = new Person { Name = "Anders", Age = 47 };
  13:  
  14:         int age = zerocool.Age;
  15:         zerocool.Age = 22;
  16:  
  17:         Person guru = anders;
  18:         anders.Name = "Anders  Hejlsberg";
  19:  
  20:         Console.WriteLine("zerocool's age:\t{0}", zerocool.Age);
  21:         Console.WriteLine("age's value:\t{0}", age);
  22:         Console.WriteLine("anders' name:\t{0}", anders.Name);
  23:         Console.WriteLine("guru' name:\t{0}", guru.Name);
  24:     }
  25: }

 

上面這段代碼,我們首先創建了一個Person類,包含了Name和Age兩個屬性,毋庸置疑,Person類是引用類型,Name也是,因爲它是string類型的(但string是很特殊的引用類型,後面將專門有一篇文章來討論),但Age則是值類型。接下來我們來看看Demonstration方法,其中演示的就是值類型跟引用類型的區別。

首先,我們聲明瞭兩個Person類的實例對象,zerocool和anders,前面提到過,這兩個對象都被分配在堆上,而zerocool和anders本身其實只是對象所在內存區域的起始地址引用,換句話說就是指向這裏的指針。我們聲明對象實例時也順便分別進行了初始化,首先我們看,zerocool對象的值類型成員,我們賦值爲25(對,我今年25歲),anders(待會兒你們就知道是誰了)的Name屬性,我們賦值爲“Anders”。齊活兒,接下來看我們怎麼幹吧。

我們聲明一個值類型變量age,直接在初始化時把zerocool的Age值賦給它,顯然,age的值就是25了。但這個時候zerocool不高興了,他想裝嫩,私自把自己的年齡改成22歲,剛夠法定結婚年齡。然後我們又聲明瞭一個引用類型的guy對象,初始化時就把anders賦給它,然後anders露出廬山真面目了,他的名字叫“Anders Hejlsberg”(在此向C#之父致敬)。接下來我們來分別答應出這幾個變量的值,看看有什麼差別。

你可能要覺得奇怪(你要不覺得奇怪,也就不用再接着往下看了),爲什麼我們改了zerocool.Age的值,age沒跟着變,改了anders.Name的值,guru.Name卻跟着變了呢?這就是值類型和引用類型的區別。我們聲明age值類型變量,並將zerocool.Age賦給它,編譯器在棧上分配了一塊空間,然後把zerocool.Age的值填進去,僅此而已,二者並無任何牽連,就像複印機一樣,只是把zerocool.Age的值拷貝給age了。而引用類型不一樣,我們在聲明guy的時候把anders賦給它,前面說過,引用類型包含的是隻想堆上數據區域地址的引用,其實就是把anders的引用也賦給guy了,因此這二者從此指向了同一塊內存區域,既然是指向同一塊區域,那麼甭管誰動了裏面的“奶酪”,另一個變現出來的結果也會跟着變,就像信用卡跟親情卡一樣,用親情卡取了錢,與之關聯的信用卡賬上也會跟着發生變化。一提到錢,估計大家夥兒印象就深了些吧,呵呵!

另外,性能上也會有區別的。既然一個是直接操作內存,另一個則多一步先解析引用地址,那麼顯然很多時候值類型會減小系統性能開銷。但“很多時候”不代表“所有時候”,有些時候還得量力而爲,例如需要大量進行函數參數傳遞或返回的時候,老是這樣進行字段拷貝,其實反而會降低應用程序性能。另外,如果實例會被頻繁地用於Hashtable或者ArrayList之類的集合中,這些類會對其中的值類型變量進行裝箱操作,這也會導致額外的內存分配和內存拷貝操作,從應用程序性能方面來看,其實也不划算。

哦對了,上面提到了一個概念,裝箱。那麼什麼是裝箱呢?其實裝箱就是值類型到引用類型的轉化過程。將一個值類型變量裝箱成一個引用類型變量,首先會在託管堆上爲新的引用類型變量分配內存空間,然後將值類型變量拷貝到託管堆上新分配的對象內存中,最後返回新分配的對象內存地址。裝箱操作是可逆的,所以還有拆箱操作。拆箱操作獲取指向對象中包含值類型部分的指針,然後由程序員手動將其對應的值拷貝給值類型變量。接下來我們來看看典型的裝箱和拆箱操作。

   1: public static class BoxingAndUnboxing
   2: {
   3:     public static void Demonstration()
   4:     {
   5:         int ageInt = new int();
   6:  
   7:         // Boxing operation.
   8:         object ageObject = ageInt;
   9:  
  10:         //ageObject = null;
  11:  
  12:         // Unboxing operation.
  13:         ageInt = (int)ageObject;
  14:  
  15:         Console.WriteLine(ageInt);
  16:     }
  17: }

在該方法中,我們首先聲明瞭一個值類型變量ageInt,但並未給它賦值,接着聲明瞭一個典型的引用類型變量ageObject,並把ageInt賦給它,這裏就進行了一次裝箱操作。編譯器現在託管堆上分配一塊內存空間(空間大小爲對象中包含的值類型變量所佔空間總和外加一個方法表指針和一個SyncBlockIndex),然後把ageInt拷貝到這個空間中,再返回該空間的引用地址。接下來第13行則是拆箱操作,編譯器獲取到ageObject對象中值類型變量的指針,然後將其值拷貝給值類型變量。如果你把第10行註釋掉的代碼打開(這是通俗說法,其實就是取消註釋),那麼第13行就會拋出System.NullReferenceException異常,要說問什麼,這又會牽扯出值類型跟引用類型另一個大的不同。看見了吧,聲明ageInt時並沒有賦值,如果關掉第10行代碼,程序不會報錯,最後打印出個0,這說明在聲明值類型變量時,如果沒有初始化賦值,編譯器會自動將其賦值爲0,既然值類型沒有引用,那麼它就不可能爲空。引用類型不一樣,它可以爲空引用,一張過期作廢的銀行卡是可以存在。而如果將一個空的對象拆箱,編譯器上哪兒去找它裏面的值類型變量的指針呢?所以這也是拆箱操作需要注意的地方。

最後,我們在把值類型和引用類型之間其它一些明顯區別大致羅列如下,以便大家能順利通過面試第一問。

  • 所有值類型都繼承自System.ValueType,但是ValueType沒有附加System.Object包含之外其它任何方法,不過它倒是改寫了Equals和GetHashCode兩個方法。引用類型變量的Equals比較的是二者的引用地址而不是內部的值,值類型變量的Equals方法比較的是二者的值而不是……哦對了,值類型壓根兒沒有引用地址;
  • 值類型不能作爲其它任何類型的基類型,因此不能向值類型中增加任何新的虛方法,更不該有任何抽象方法,所有的方法都是sealed的(不可重寫);
  • 未裝箱的值類型分配在棧上而不是堆上,而棧又不是GC的地盤兒,因此GC根本不過問值類型變量的死活,一旦值類型變量的作用範圍一過,它所佔的內存空間就立即被回收掉,不勞GC親自動手。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章