1. 程式人生 > >c#中的引用類型和值類型

c#中的引用類型和值類型

有意義 tle 線程同步 pan trac 理解 也會 自己的 方法

一,c#中的值類型和引用類型

眾所周知在c#中有兩種基本類型,它們分別是值類型和引用類型;而每種類型都可以細分為如下類型:

技術分享圖片

  1. 什麽是值類型和引用類型
    • 什麽是值類型:
      • 進一步研究文檔,你會發現所有的結構都是抽象類型System.ValueType的直接派生類,而System.ValueType本身又是直接從System.Object派生的。根據定義所知,所有的值類型都必須從System.ValueType派生,所有的枚舉都從System.Enum抽象類派生,而後者又從System.ValueType派生。
      • 所有的值類型都是隱式密封的(sealed),目的是防止其他任何類型從值類型進行派生。
    • 什麽是引用類型:
      • 在c#中所有的類都是引用類型,包括接口。
  2. 區別和性能
    • 區別:
      • 值類型通常被人們稱為輕量級的類型,因為在大多數情況下,值類型的的實例都分配在線程棧中,因此它不受垃圾回收的控制,緩解了托管堆中的壓力,減少了應用程序的垃圾回收的次數,提高性能。
      • 所有的引用類型的實例都分配在托管堆上,c#中new操作符會返回一個內存地址指向當前的對象。所以當你在創建個一個引用類型實例的時候,你必須要考慮以下問題:
        • 內存是在托管堆上分配的
        • 在分配每一個對象時都會包含一些額外的成員(類型對象指針,同步塊索引),這些成員必須初始化
        • 對象中的其他字節總是設為零
        • 在分配對象時,可能會進行一次垃圾回收操作(如果托管堆上的內存不夠分配一次對象時)
    • 性能:
      • 在設計一個應用程序時,如果都是應用類型,那麽應用程序的性能將顯著下降,因為這會加大托管堆的壓力,增加垃圾回收的次數。
      • 雖然值類型是一個輕量級的類型,但是如果大量的使用值類型的話,也會有損應用程序的性能(例如下面要講的裝箱和拆箱操作,傳遞實例較大的值類型,或者返回較大的值類型實例)。
      • 由於值類型實例的值是自己本身,而引用類型的實例的值是一個引用,所以如果將一個值類型的變量賦值給另一個值類型的變量,會執行一次逐字段的復制,將引用類型的變量賦值給另一個引用類型的變量時,只需要復制內存地址,所以在對大對象進行賦值時要避免使用值類型。例如下面的代碼 技術分享圖片
         1  class SomRef
         2     {
         3         public int x;
         4     }
         5     struct SomeVal {
         6         public int x;
         7     }
         8     class Program {
         9         static void ValueTypeDemo() {
        10             SomRef r1 = new SomRef();//在堆上分配
        11             SomeVal v1 = new SomeVal();//在棧上分配
        12             r1.x = 5;//提領指針
        13             v1.x = 5;//在棧上修改
        14             SomRef r2 = r1;//只復制引用(指針)
        15             SomeVal v2 = v1;//在棧上分配並復制成員
        16         }
        17     }
        技術分享圖片

        技術分享圖片

  3. 常見誤區
    • 引用類型分配在托管堆上,值類型分配在線程棧上:其實這種說法的前半部分是對的,後半部分是錯的。因為變量的值在它聲明的位置存儲的,所以假如某一個引用類型中有一個值類型的變量, 那麽該變量的值總是和該引用類型的對象的其它數據在一起,也就是分配在堆上。(只有局部變量(方法內部聲明的變量)和方法的參數在棧上)
    • 結構是輕量級的類:這種錯誤的信息主要是因為有人認為值類型不應該有方法或者其它有意義的行為-它們應該作為簡單的數據轉移來使用,所以很多人分不清DateTime到底是值類型還是引用類型。
    • 對象在c#中默認的是用過引用傳遞的:其實在調用方法的時候,參數值(對象的一個引用)是以傳值得方式傳遞的,如果你想以引用方式傳遞的話,可以使用ref或者out關鍵字。

二,值類型的裝箱和拆箱操作

1 int i = 5;
2 object o = i;
3 int j = (int)o;
4 Int16 y=(Int16)o;

  1. 什麽是裝箱,什麽是拆箱
    • 什麽是裝箱:所謂裝箱就是將值類型轉化為引用類型的過程(例如上面代碼的第2行),在裝箱時,你需要知道編譯器內部都幹了什麽事:
      • 在托管堆中分配好內存,分配的內存量是值類型的各個字段需要的內存量加上托管堆上所以對象的兩個額外成員(類型對象指針,同步塊索引)需要的內存量
      • 值類型的字段復制到新分配的堆內存中
      • 返回對象的地址,這個地址就是這個對象的引用
    • 什麽是裝箱:將已裝箱的值類型實例(此時它已經是引用類型了)轉化成值類型的過程(例如上面代碼的第3行),註意:拆箱不是直接將裝箱過程倒過來,拆箱的代價比裝箱要低的多,拆箱其實就是獲取一個指針的過程。一個已裝箱的實例在拆箱時,編譯器在內部都幹了下面這些事:
      • 如果包含了“對已裝箱類型的實例引用”的變量為null時,會拋出一個NullReferenceException異常。
      • 如果引用指向的對象不是所期待的值類型的一個已裝箱實例,會拋出一個InvalidCastException異常(例如上面代碼的第4行)。
  1. 它們在什麽情況下發生,以及如何避免
  2. 技術分享圖片
    1    static void Main(string[] args)
    2         {
    3             int v = 5;
    4             object o = v;
    5             v = 123;
    6             Console.WriteLine(v+","+(int)o);
    7        }
    技術分享圖片

    通過上面的分析我們已經知道了,裝箱和拆箱/復制操作會對應用程序的速度和內存消耗產生不利的影響(例如消耗內存,增加垃圾回收次數,復制操作),所以我們應該註意編譯器在什麽時候會生成代碼來自動這些操作,並嘗試手寫這些代碼,盡量避免自動生成代碼的情況。

    • 你能一眼從上面的代碼中看出進行了幾次裝箱操作嗎?正取答案是3次。分別進行了哪三次呢,我們來看一下:第一次object o=v;第二次在執行 Console.WriteLine(v+","+(int)o);時將v進行裝箱,然後對o進行拆箱後又裝箱。也就是說裝箱過程總是在我們不經意的時候進行的,所以只有我們充分了解了裝箱的內部機制,才能有效的避免裝箱操作,從而提高應用程序的性能。所以對上面的代碼進行如下修改可以減少裝箱次數,從而提高性能:

      技術分享圖片
      1  static void Main(string[] args)
      2         {
      3             int v = 5;
      4             object o = v;
      5             v = 123;
      6             Console.WriteLine(v.ToString() + "," + ((int)o).ToString());//((int)o).ToString()代碼本身沒有任何意義,只為演示裝箱和拆箱操作
      7        }
      技術分享圖片
    • 下面來討論一下編譯器都會在什麽時候自動生成代碼來完成這些操作
      • 使用非泛型集合時:比如ArrayList,因為這些集合需要的對象都是object,如果你將一個值類型的對象添加到集合中時會執行一次裝箱操作,當你取值時會執行一次拆箱操作,所以在應用程序中應避免使用這種非泛型的集合。
      • 大家都知道System.Object是所有類型的基類,當你調用object類型的非虛方法時會進行裝箱操作(例如GetType方法)。在調用object的虛方法時,如果你的值類型沒有重寫虛方法也要進行裝箱操作,所以在定義自己的值類型時,應重寫object內部的虛方法(例如ToString方式)
      • 將值類型轉化為接口類型時也會進行裝箱操作,這是因為接口類型必須包含對堆上的一個對象的引用。

三,泛型的出現(本節只簡單介紹泛型對裝箱和拆箱所起的作用,關於泛型的具體細節請參考下一篇文章)

    • 什麽泛型
      • 泛型是CLR和編程語言提供的一種特殊機制,它在c#2中才被提供出來。
    • 它對避免裝箱有什麽作用?
      • 在使用泛型時需要指定要裝配的類型,這樣可以減少裝箱操作,比如下面的代碼 技術分享圖片
         1   static void Main(string[] args)
         2         {
         3             ArrayList dateList = new ArrayList { 
         4             DateTime.Now
         5             };
         6 
         7             IList<DateTime> dateT = new List<DateTime> { 
         8             DateTime.Now
         9             };
        10         }
        技術分享圖片

        使用ArrayList時,每添加一個時間都會進行一次裝箱操作,而使用List<DateTime>時就不會進行裝箱操作,從而提高應用程序的性能。

    • C#中常見的泛型集合:

      Queue<T>;

      Stack<T>;

      List<T>;

      Dictionary<Tkey,Tvalue>;

      HashSet<T>;

      在使用這些集合之前我們必須要理解每一種集合的工作原理(沒事自己可以實現一下),了解每一種集合的適合場合,這樣才能寫出高效的代碼。

四,在設計時如何選擇類和結構體

在面試的時候,我們經常被問的一個問題(還有另外一個問題,如何選擇抽象類和接口,下次我會單獨聊聊這個問題),下面我們來聊聊在設計時應該如何選擇結構體和類

    • 什麽是結構體
      • 結構體是一種特殊的值類型,所以它擁有值類型所以的特權(實例一般分配在線程棧上)和限制(不能被派生,所以沒有 abstract 和 sealed,未裝箱的實例不能進行線程同步的訪問)。
    • 什麽情況下選擇結構體,什麽情況下選擇類
      • 在大多數的情況下,都應該選擇類,除非滿足以下情況,才考慮選擇結構體:
      • 類型具有基元類型的行為
      • 類型不需要從其它任何類型繼承
      • 類型也不會派生出任何其它類型
      • 類型的實例較小(約為16字節或者更小)
      • 類型的實例較大,但是不作為方法的參數傳遞,也不作為方法的返回值。

都說程序是一門註重實踐的學科,但是也只有熟悉理解了這些概論的東西,才能在實踐時寫出優秀的代碼,有不對或者不合理的地方歡迎在下面討論;

c#中的引用類型和值類型