C#筆記 – 自定義類型

類型成員
類型可見性
  • public:對定義程序集中及其他程序集中的代碼可見

  • internal:僅對定義程序集中的代碼可見

  • 只想對定義程序集中以及「定義程序集外的某些特定程序集可見」

    • 友元程序集

    • 使用System.Runtime.CompilerServices中的InternalsVisibleTo標明認為是「友元」的其他程序集

      • 參數:友元程序集名 + 公鑰字符串

    • 編譯友元程序集要求使用 /out:

    • 使用/t:module來編譯友元程序集的一部分模塊時,需使用/moduleassemblyname開關來編譯

成員可訪問性
  • CLRC#Desc
    privateprivate成員只能由定義類型/嵌套類型中的方法訪問
    Familyprotected成員只能由定義類型/嵌套類型/任何程序集中的派生類型的方法訪問
    Family and Assembly成員只能由(定義類型/嵌套類型)或(同一程序集)中的派生類型的方法訪問
    Assemblyinternal成員只能由定義程序集中的方法訪問
    Family or Assemblyprotected internal成員可由任何嵌套類型、派生類型 或 定義程序集中的任何方法訪問
    publicpublic可由任何程序集的任何方法訪問
  • 派生類重寫基類成員時,C#要求原始成員和重寫成員具有相同的可訪問性

    • 而CLR其實沒有這個限制,CLR的規定時,從基類派生時,派生類可以放寬基類的可訪問性,但不可以收緊

    • 因為CLR承諾派生類型總能轉型為基類,獲取對基類的訪問權

靜態類
  • 一些永遠不需要實例化的類,用於組合一組相關的成員

  • static關鍵字

    • 只能應用於類,不能應用於結構體(值類型)

      • 因為CLR總是允許值類型實例化

  • C#編譯器對靜態類的限制

    • 靜態類必須直接從System.Object派生,從其他任何基類派生都沒有意義。

      • 因為繼承只適用於對象,但是我不能創建靜態類的實例,因此沒有意義

    • 靜態類不能實現接口,因為只有使用類的實例時,才可以調用類的接口方法

    • 靜態類只能定義靜態成員

      • 不能包含任何protected/protected internal成員
      • 不能包含任何非靜態成員/構造函數
    • 靜態類不能作為字段、方法參數、局部變量使用,因為它們都代表引用了實例的變量

    • 不能包含任何操作符

  • IL中的靜態類

    • //先在C#代碼定義一個靜態類
      public static class AStaticClass
      {
         public static void AStaticMethod() { }


         static string s_AStaticField;
         public static string AStaticField;
         public static string AStaticProperty
        {
             get { return s_AStaticField; }
             set { s_AStaticField = value; }
        }
      }
    • 該類型IL中,被標記為abstract和sealed;而且沒有生成實例構造器方法(.ctor)

      • 所以也不能聲明為abstract/sealed
組件、多態、版控
  • 組件軟件編程(Component Software Programming,CSP)

    • OOP發展到極致的結果

    • 「組件」(.NET框架中稱為程序集)的特點

      • 有「已經發布」的意思

      • 有自己的標識(如名稱、版本、語言文化、公鑰)

      • 永遠維持自己的標識(代碼不會靜態鏈接到另一個程序集中,總是使用動態鏈接)

      • 清楚指明所依賴的組件(引用元數據表)

      • 編檔它的類和成員(XML文檔/doc命令行)

      • 需要指定其安全權限(CLR的代碼訪問安全性,CAS)

      • 組件要發布在任何「維護版本」中的都不會改變的接口

        • 「維護版本」代表組件的新版本,向後兼容組件的原始版本

        • 一般包含bug fix、patch

        • 不包含新的依賴關係、附加的安全權限

    • C#通過一些應用在類型/類型成員的關鍵字來影響組件版本控制

      • 在多態的情況下,一個程序集中定義的類型作為另一個程序集的一個類型的基類使用時,可能發生版控問題,導致派生類行為失常

      • 尤其是派生類會重寫基類的虛方法時,問題會更明顯

      • 關鍵字

        • keywordClass方法/屬性/事件const/field
          abstract表示不能實例化表示子類必須實現該成員N/A
          virtualN/A表示子類可以實現該成員N/A
          overrideN/A表示子類正在重寫基類成員N/A
          sealed表示不能被繼承表示不能被子類重寫(該關鍵字只能應用於重寫虛方法的方法)N/A
          new表示該成員與基類相似成員無任何關係表示該成員與基類相似成員無任何關係表示該成員與基類相似成員無任何關係
CLR對虛方法/屬性/事件的調用
  • internal class Employee
    {
       public int GetYearsEmployed() { return 0; }
       public virtual string GetProgressReport() { return ""; }
       public static Employee Lookup(string name) { return null; }
    }
  • 在調用上述方法時,編譯器會檢查方法定義的flag,並判斷如何生成IL代碼來調用這些方法

    • Flag

    • CLR提供了2個方法調用指令

      • call

        • 可調用靜態方法

          • 必須指定方法的定義類型

        • 可調用實例方法/虛方法

          • 必須指定引用了對象的變量

          • 假定變量 != null

          • 變量本身的類型指明了方法的定義類型

            • 如果變量的類型沒有定義方法,就檢查其基類

        • call經常用於非虛調用虛方法

      • callvirt

        • 可調用實例方法/虛方法

          • 必須指定引用了對象的變量

        • 調用非虛實例方法時

          • 變量類型指明了方法的定義類型

        • 調用虛實例方法時

          • CLR調查發出調用的對象的實際類型,然後以多態方式調用

        • 為確定類型,發出調用的變量絕不能為null

          • 因此,編譯該調用時,JIT會生成驗證代碼,導致callvirt在效率上比call稍慢

          • 即使callvirt調用的是非虛實例方法,也要執行該null檢查

    • 例子:

      • //C#
        static void Main(string[] args)
        {
           Console.WriteLine(); //Call static method - call
           object o = new object();
           o.GetHashCode(); //Call virtual instance method - callvirt
           o.GetType(); //Call non virtual instance method - callvirt
        }
      • //IL
        IL_0000: nop
        IL_0001: call       void [System.Console]System.Console::WriteLine()
        IL_0006: nop
        IL_0007: newobj     instance void [System.Runtime]System.Object::.ctor()
        IL_000c: stloc.0
        IL_000d: ldloc.0
        IL_000e: callvirt   instance int32 [System.Runtime]System.Object::GetHashCode()
        IL_0013: pop
        IL_0014: ldloc.0
        IL_0015: callvirt   instance class [System.Runtime]System.Type [System.Runtime]System.Object::GetType()
        IL_001a: pop
        • 調用Console.WriteLine時,由於是靜態方法,因此命令為call

        • 調用GetHashCode時,由於是虛方法,因此命令為callvirt

        • 調用GetType時,雖然是不是虛方法,但是仍然使用了callvirt命令

          • 在JIT編譯時,CLR知道GetType不是虛方法,因此會直接以非虛方式調用GetType

          • 因為C#團隊認為,JIT應生成代碼來驗證發出調用的對象不為null

          • 因此就結果而言,靜態方法用call來調用,而非靜態方法,無論是否非虛實例方法,一律通過callvirt調用

            • 從而推出,調用靜態方法必然比調用對象方法快

        • 另一種用call來調用虛方法的情況,是在重寫的方法中調用基類的同名方法

          • public class BaseObject
            {
               public virtual void BaseTest() { }
            }

            public class TesterC : BaseObject
            {
               public override void BaseTest()
              {
                   base.BaseTest();
              }
            }
          • .method public hidebysig virtual instance string 
                  ToString() cil managed
            {
            // 程式碼大小       12 (0xc)
            .maxstack 1
            .locals init (string V_0)
            IL_0000: nop
            IL_0001: ldarg.0
            IL_0002: call       instance string [System.Runtime]System.Object::ToString()
            IL_0007: stloc.0
            IL_0008: br.s       IL_000a
            IL_000a: ldloc.0
            IL_000b: ret
            } // end of method TesterC::ToString
          • TesterC調用虛方法BaseTest時,C#生成call指令來確保以非虛方式調用基類的BaseTest方法

            • 因為如果以callvirt的指令調用,調用會遞歸執行,直接線程棧溢出

              • 因為調用虛實例方法時,CLR調查發出調用的對象的實際類型,然後以多態方式調用,也就是調用自己

      • 另外,編譯器在調用值類型定義的方法時,傾向於使用call指令,因為:

        • 值是密封的,不用考慮虛方法的多態性

        • 值類型實例一般永不為null

        • 以虛方式調用值類型虛方法,需要獲取值類型的類型對象引用,發生裝箱

      • 無論是call還是callvirt調用實例/虛方法,這些方法都接收隱藏的this實作為方法第一個參數,this引用指向操作對象

參考書目

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