C#筆記 – 構造方法

實例構造器

  • 構造器方法在方法定義元數據表中為「.ctor」

  • 初始化實例字段

    • public SomeType(){}
      public SomeType(int value){}
    • //Check in ildasm
      .ctor : void()
      .ctor : void(int32)
  • 引用類型實例
    • 創建實例時:

      • 首先為實例的數據字段分配內存

      • 然後初始化對象的附加字段(同步塊索引和類型對象指針)

      • 將為對象分配的內存中的字段的值設為0或null

      • 最後調用類型的實例構造方法

    • 實例構造方法永遠不能被繼承,因此實例構造方法不可以使用virtual、new、override、sealed、abstract修飾符

    • 如果類沒有顯式定義任何實例構造器,C#編譯器會定義一個默認的public無參實例構造器

      • 該默認實例構造器只簡單調用了基類的無參實例構造器

      • 如果類為抽象類(abstract),默認實例構造器的可訪問性就為protected;且派生類必須顯式調用一個實例構造器

      • 如果類為靜態類(static,IL中為sealed+abstract),那這個類根本不需要實例構造器,因此編譯器不會在類的定義中生成默認實例構造器

    • 類的實例構造器在訪問從基類繼承的任何字段前,都須先調用基類的實例構造器;如果沒有顯式調用,C#編譯器就會自動生成對默認的基類實例構造器的調用

      • 最終,System.Object的公共無參實例構造器會被調用

    • 不調用實例構造器下創建實例的少數例子

      • Object.MemberwiseClone => 分配內存,初始化附加字段,將源對象字段數據複製到新對象中

      • 運行時序列化器反序列化對象 => 使用GetUninitializedObject / GetSafeUninitializedObject為對象分配內存,不調用構造器

    • 不要在實例構造器中調用虛方法,假如被實例化的類型重寫了虛方法,就會執行派生類型對虛方法的實現

      • 而這時尚未完成對繼承層次結構中的所有字段的初始化,導致無法預測的行為發生

  • 值類型實例
    • 由於CLR總是允許創建值類型的實例,而且沒辦法阻止,因此不需要定義實例構造器,C#編譯器也不會為值類型嵌入默認的實例構造器

      • internal struct BaseValue
        {
           public int x;
           public int y;
        }
    • 假如值類型在引用類型中被聲明,為了構造該引用類型,必須使用new來創建引用類型的實例
      • 而在為引用類型創建實例時,內存中會包含值類型的字段,且字段值會被初始化為0或null,但這不是因為CLR為這些字段主動調用了實例構造器

        • 嚴格來說,只有嵌套到引用類型中的值類型字段會被保證初始化為0或null

        • 其他基於棧的值類型字段必須在讀取之前賦值

      • CLR允許為值類型定義實例構造器,但必須顯式調用才會執行

        • 為了防止開發人員對值類型的構造器甚麼時候會被調用產生懷疑,C#編譯器不允許值類型定義無參的實例構造器

          • 與此同時,由於值類型不能定義無參實例構造器,值類型也不能使用「內聯語法」初始化字段

        • 因此,CLR只允許值類型定義有參的實例構造器,而且這個構造器必須初始化值類型的「全部字段」

          • internal struct Point
            {
               //public int m_x = 100; //無效
               public int m_x;
               public int m_y;        


               //無效
               //public Point()
               //{
               //   m_x = 5;
               //   m_y = 5;
               //}

               public Point(int x, int y)
              {
                   m_x = x;
                   m_y = y;
              }

               //編譯不通過
               //public Point(int x)
               //{
               //   m_x = x;
               //}

               //編譯不通過,至少包含1個參數
               //public Point()
               //{
               //   this = new Point();
               //}

               public Point(int y)
              {
                   this = new Point();
                   m_y = y;
              }
            }
          • 在值類型的構造器中,this代表值類型本身的一個實例,用new創建的值類型實例可以賦給this

            • 在new的過程中,會把所有字段設為0或null

            • 在引用類型的實例構造器中,this是唯讀的;而在值類型的實例構造器中,this是可讀可寫的

類構造器
  • 初始化靜態字段

  • static SomeType(){}
  • //Check in ildasm
    .cctor : void()
  • 用於設置類型的初始狀態,可以應用於接口、值類型、引用類型

    • 但是永遠都不要定義值類型的構造器,因為CLR有時不會調用值類型的靜態類型構造器

  • 類默認沒有定義類構造器,如果定義,也只能定義一個

  • 類構造器永遠沒有參數

  • 總是私有(private)的,而且必須標記為靜態(static)

  • 類構造器中的代碼只能訪問類型的靜態字段,初始化這些字段也是它的常規用途

    • 雖然C#不允許值類型在實例字段上使用內聯語法,但是靜態字段可以

    • 如果這樣做,C#就會自動為類生成一個類構造器

    • 如果靜態字段同時使用了內聯語法和在類構造器中賦值,C#編譯器會先使用內聯語法賦值,然後再用類構造器中的值覆蓋掉內聯語法賦的值

  • 引用類

    • C#

      • //引用類構造
        internal sealed class SomeRefType
        {
           private static int s_x = 5;
           private static int s_y;
           static SomeRefType()
          {
               //Do Some Class Init Step
               s_y = 10;
          }
        }
    • IL

      • .method private hidebysig specialname rtspecialname static 
              void .cctor() cil managed
        {
        // 程式碼大小       15 (0xf)
        .maxstack 8
        IL_0000: ldc.i4.5
        IL_0001: stsfld     int32 CLR_Ch8.SomeRefType::s_x
        IL_0006: nop
        IL_0007: ldc.i4.s   10
        IL_0009: stsfld     int32 CLR_Ch8.SomeRefType::s_y
        IL_000e: ret
        } // end of method SomeRefType::.cctor
  • 值類

    • C#

      • //值類構造
        internal struct SomeValType
        {
           private static int s_x = 5;
           private static int s_y;
           static SomeValType()
          {
               //Do Some Value Init Step
               s_y = 10;
          }
        }
    • IL

      • .method private hidebysig specialname rtspecialname static 
              void .cctor() cil managed
        {
        // 程式碼大小       15 (0xf)
        .maxstack 8
        IL_0000: nop
        IL_0001: ldc.i4.5
        IL_0002: stsfld     int32 CLR_Ch8.SomeValType::s_x
        IL_0007: ldc.i4.s   10
        IL_0009: stsfld     int32 CLR_Ch8.SomeValType::s_y
        IL_000e: ret
        } // end of method SomeValType::.cctor
  • 類構造器的調用

    • JIT編譯方法會檢查其所引用的類型

    • 任何一個類型定義了類構造器,JIT會針對當前AppDomain檢查是否己經執行過類構造器

      • 如果沒有執行過,JIT會在它生成的本機代碼中添加對類構造器的調用;否則就不添加對它的調用

    • 當方法被JIT編譯完後,線程開始執行,最終執行到類構造器的調用代碼上

    • 多個線程可能同時執行相同的方法,CLR希望確保在每個AppDomain中,一個類構造器只執行一次

      • 因此,在調用類構造器時,調用線程獲取一個互斥線程同步鎖,從而阻止多個1個線程同時調用類構造器

      • 所以,CLR對類構造器的執行是「線程安全」的,使得在類型構造器中初始化類型需要的任何單例對象是一個不錯的選擇

      • 理所當然的,不要讓兩個類構造器互相引用

      • 由於不可能有靜態字段從基類分享/繼承,因此不應調用基類的類構造器

  • 雖然有類的構造器,但是沒有類的析構器

    • 類只有在AppDomain卸載時才會卸載,而AppDomain卸載時,會使類成為不可達的對象,並被GC回收

    • CLR也不支持靜態Finalize方法

    • 可以考慮在System.AppDomain.DomainUnload事件登記回調方法

參考書目

  • 《CLR via C#》(第4版) Jeffrey Richter