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

一,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關鍵字。

二,值類型的裝箱和拆箱操做c#

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方式)
      • 將值類型轉化爲接口類型時也會進行裝箱操做,這是由於接口類型必須包含對堆上的一個對象的引用。

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

    •  什麼泛型
      • 泛型是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>;3d

      List<T>;指針

      Dictionary<Tkey,Tvalue>;

      HashSet<T>;

       

       在使用這些集合以前咱們必需要理解每一種集合的工做原理(沒事本身能夠實現一下),瞭解每一種集合的適合場合,這樣才能寫出高效的代碼。

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

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

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

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

相關文章
相關標籤/搜索