C#筆記 – 類型基礎(四) 不同類型對象的存儲位置

引用類型和值類型
  • CLR支持引用類型和值類型

    • FCL大多類型都是引用類型

  • 所有的class都是引用類型;所有的struct/enum都是值類型

    • 所有的struct都是抽象類型System.ValueType的直接派生類,System.ValueType又直接從System.Object派生

    • 所有的enum從System.Enum派生,System.Enum又派生自System.ValueType

    • 因此,自定義值類型不能選擇基類(已有ValueType),但可以實現接口

      • 另外,所有值類型都隱式密封,無法被繼承,以防作為其他引用類型/值類型的基類

  • 引用類型的值永遠是對堆地址對象的引用;值類型的值永遠是值本身
  • 引用類型

    • 內存從托管堆分配,C#的 new操作符返回對象內存地址

    • 堆上分配的每個對象都有一些額外成員(如:類型對象指針、同步塊索引),這些成員必須初始化

    • 對象中的其他字節(為字段而設)總是設為0

    • 從托管堆分配對象時,可能強制執行一次GC

  • 因此,引用類型會比較「重量級」,為了減輕程序的負擔,CLR提供了較「輕量」的值類型

  • 值類型

    • 實例一般在線程棧上分配(有時候會作為字段嵌入引用類型的對象中),而不在托管堆上分配

      • 因此也沒有堆上每個對象都有的兩個額外成員(類型對象指針和同步塊索引)
    • 代表值類型實例的變量中不包含指向實例的指針,而是包含了實例本身的字段。因此操作實例中的字段不需要通過指針引用

    • 值類型實例不受GC控制,使用值類型可緩解托管堆的壓力,減少了應用程序生存期內的GC回收次數

    • 因此,值類型比引用類型「輕」

選擇值類型還是引用類型
  • 除非滿足以下全部條件,否則不應將類型聲明為值類型

    • 類型簡單,沒有成員會修改類型的任何實例字段(不可變的)

      • 建議值類型的全部字段都設為readonly

    • 不需要繼承其他類型也不需要被任何類型所繼承

  • 考慮類型實例大小

    • 實參默認以傳值方式傳遞,因此會對值類型實例中的字段進行複製

    • 返回一個值類型的方法在返回時,實例中的字段也會複製到調用者分配的內存中

    • 因此,如果類型的實例較大,就不應使用值類型,起碼不應作用方法實參/返回值

      • 較小的標準(16字節以下)

  • 值類型與引用類型的區別

    • 值類型對象有兩種表示形式:未裝箱/已裝箱;引用類型總是處於已裝箱形式

    • 值類型從System.ValueType派生,其提供了與System.Object相同的方法,重寫了Equals方法和GetHashCode方法

      • Equals:在兩個對象的字段值完全匹配前提下返回true

      • GetHashCode:生成哈希碼時,會將對象的實例字段中的值考慮在內

      • 自定義值類型時應重寫這兩個方法

    • 值類型不能作為基類,因此不應在值類型裡定義虛/抽象方法,需要保持隱式密封

    • 引用類型變量包含堆中對象的地址,創建時默認初始化為null;值類型變量總是包含其基礎類型的一個值,所有成員都初始化為0

    • 將值類型變量賦值給另一個值類型變量,會執行逐字段的複製;而一個引用變量賦給另一個引用變量,則只複製內存地址

      • 因此,在賦值的效率上,引用類型比值類型更高

      • 同時,由此可見,多個引用變量可以引用至同一個內存地址,因此對一個變量執行的操作可能影響到另一個變量引用的對象;相反,值類型變量自成一體,對一個值類型的操作不可能影響到另一個值類型變量

    • 由於未裝箱的值類型不在堆上分配,一旦定義了該類型的一個實例方法不再活動,為它們分配的內存就會馬上被釋放,而不是像引用類型一樣等待回收

存儲位置
  • 引用類型實例總是存儲在堆中,靜態變量也是

    • 引用類型的值是引用,而不是對象本身

  • 值類型在他聲明的位置存儲

    • 局部變量總是存儲在棧中

    • 實例變量總是存儲在實例本身存儲的地方中

      • 如果類中有一個int類型的實例變量,那這個int總是和這個類的實例對象中的其他數據在一起,也就是在堆上。

  • 值類型不可以派生出其他類型,因此值不需要額外的信息描述值實際的類型

  • 引用類型的每個對象開頭都包含一個標識其實際類型的數據塊,還提供了其他一些信息。

    • 永遠不能改變對象的類型。強制轉換時,運行時會獲取一個引用,檢查它引用的對象是不是目標類型的一個有效對象,如果有效,就返回原始引用

    • Stream stream = new MemoryStream();
      MemoryStream ms = (MemoryStream)stream; //檢查stream的引用是不是一個MemoryStream兼容的類型,將ms的引用設為stream的引
  •  

參考書目

  • 《CLR via C#》(第4版) Jeffrey Richter
  • 《深入理解C#》(第3版) Jon Skeet