C#筆記 – 擴展方法

擴展方法
  • 擴展方法允許定義一個靜態方法,但是用實例方法的語法來調用,使代碼的行為順序看上去更合理

  • public static int IndexOf(this StringBuilder sb, char value)
    {
       for (int i = 0; i < sb.Length; i++)
      {
           if(sb[i] == value)
          {
               return i;
          }
      }
       return -1;
    }
  • 現在,如果C#編譯器看到以下代碼:

    • StringBuilder sb = new StringBuilder();
      int idx = sb.IndexOf('a');
    • C#編譯器首先會檢查StringBuilder類或其所有基類有沒有提供對應的實例方法

    • 如果沒有,就檢查有沒有靜態類定義了對應的靜態方法,而且方法第一個參數類型為StringBuilder,並使用了this標示

  • 規則

    • C#只支持擴展「方法」

    • 擴展方法必須在非泛型靜態類中聲明

      • 至少要有一個參數,而且只有第一個參數能用this關鍵字標記

        • 第一個參數不能有其他修飾符(如out/ref)

        • 第一個參數類型(擴展類型)不能是指針類型

      • 該靜態類不能嵌套在其他類裡面,必須是「頂級靜態類」

    • C#編譯器查找靜態類時:

      • 檢查文件作用域中所有靜態類,掃瞄它們的所有靜態方法來找匹配項,可以事先使用using導入命名空間,提升查找效率

      • 如果檢測到多個同樣定樣的擴展方法,就需要顯式指定靜態類的名稱

      • 如果存在適當的實例方法,則實例方法肯定會先於擴展方法使用,並不會被編譯器警告

    • 擴展一個類型的方法時,同時也擴展了派生類

      • 不要將System.Object作為擴展方法的第一個參數

  • 擴展方法雖然是通過實例方法的語法來調用,但實際上還是一個靜態方法,所以IL生成的調用代碼是call而不是callvirt

    • 因此,CLR也不會生成對調用方法的表達式的值進行null檢查的代碼

      • 在C#中,不能在空引用上調用實例方法,但可以調用擴展方法
    • StringBuilder sb = null;
      sb.IndexOf('c'); //throw exception inside IndexOf
      sb.Replace('.', '!'); //throw exception in this command line
    • .method private hidebysig static void  Main(string[] args) cil managed
      {
      .entrypoint
      // 程式碼大小       48 (0x30)
      .maxstack 3
      .locals init (class [System.Runtime]System.Text.StringBuilder V_0,
                class [System.Runtime]System.Action V_1)
      IL_0000: nop
      IL_0001: ldnull
      IL_0002: stloc.0
      IL_0003: ldloc.0
      IL_0004: ldc.i4.s   99
      IL_0006: call       int32 CLR_Ch8.Extensions::IndexOf(class [System.Runtime]System.Text.StringBuilder,
                                                              char)
      IL_000b: pop
      IL_000c: ldloc.0
      IL_000d: ldc.i4.s   46
      IL_000f: ldc.i4.s   33
      IL_0011: callvirt   instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Replace(char,
                                                                                                                                        char)
      IL_0016: pop
      } // end of method Program::Main
  • 接口定義的擴展方法

    • //Extension on Interface
      public static void ShowItems<T>(this IEnumerable<T> collection)
      {
         foreach (var item in collection)
        {
             Console.WriteLine(item);
        }
      }
  • 委托定義的擴展方法

    • //Extension on Delegate
      public static void InvokeAndCatch<TException>(this Action<Object> d, Object o)
         where TException : Exception
        {
             try { d(o); }
             catch (TException) { }
        }
    • 委托也可以指向一個擴展方法

      • Action a = "Jeff".ShowItems;
        a();
ExtensionAttribute
  • 查找擴展方法時,C#編譯器會檢查導入的所有命名空間和當前命名空間中的所有擴展方法,並匹配那些從表達式類型到擴展類型存在隱式轉換的擴展方法,因此編譯器必須能區分擴展方法與某靜態類中恰好具有合適簽名的其他方法

    • 檢查類和方法是否具有System.Runtime.CompileServices.ExtensionAttribute這個特性

  • 在C#中,一旦使用了this關鍵字標記某個靜態方法的第一個參數,編譯器就會在內部向該方法應用一個定制特性(System.Code.dll中的ExtensionAttribute)。該特性會在最終生成文件元數據中持久性地存儲下來

    • Method #1 (0600001a) 
      -------------------------------------------------------
      MethodName: IndexOf (0600001A)
      Flags     : [Public] [Static] [HideBySig] [ReuseSlot] (00000096)
      //...
      CustomAttribute #1 (0c00000d)
      -------------------------------------------------------
      CustomAttribute Type: 0a000001
      CustomAttributeName: System.Runtime.CompilerServices.ExtensionAttribute :: instance void .ctor()
      Length: 4
      Value : 01 00 00 00                                     >               <
      ctor args: ()
  • 任何靜態類只要包含至少一個擴展方法,它的元數據也會應用這個特性

    • TypeDef #11 (0200000c)
      -------------------------------------------------------
      TypDefName: CLR_Ch8.Extensions (0200000C)
      Flags     : [Public] [AutoLayout] [Class] [Abstract] [Sealed] [AnsiClass] [BeforeFieldInit] (00100181)
      Extends   : 0100000D [TypeRef] System.Object
      Method #1 (0600001a)
      -------------------------------------------------------
      MethodName: IndexOf (0600001A)
      //...
      CustomAttribute #1 (0c00000d)
      -------------------------------------------------------
      CustomAttribute Type: 0a000001
      CustomAttributeName: System.Runtime.CompilerServices.ExtensionAttribute :: instance void .ctor()
      Length: 4
      Value : 01 00 00 00                                     >               <
      ctor args: ()

      Method #2 (0600001b)
      -------------------------------------------------------
      MethodName: ShowItems (0600001B)
      //...
      CustomAttribute #1 (0c00000e)
      -------------------------------------------------------
      CustomAttribute Type: 0a000001
      CustomAttributeName: System.Runtime.CompilerServices.ExtensionAttribute :: instance void .ctor()
      Length: 4
      Value : 01 00 00 00                                     >               <
      ctor args: ()

      Method #3 (0600001c)
      -------------------------------------------------------
      MethodName: InvokeAndCatch (0600001C)
      //...
      (2) ParamToken : (08000017) Name : o flags: [none] (00000000)
      CustomAttribute #1 (0c00000f)
      -------------------------------------------------------
      CustomAttribute Type: 0a000001
      CustomAttributeName: System.Runtime.CompilerServices.ExtensionAttribute :: instance void .ctor()
      Length: 4
      Value : 01 00 00 00                                     >               <
      ctor args: ()

      CustomAttribute #1 (0c00000c)
      -------------------------------------------------------
      CustomAttribute Type: 0a000001
      CustomAttributeName: System.Runtime.CompilerServices.ExtensionAttribute :: instance void .ctor()
      Length: 4
      Value : 01 00 00 00                                     >               <
      ctor args: ()
  • 同樣地,任何程序集只要包含了一個這樣的靜態類,它的元數據也會應用這個特性

    • ===========================================================
      ScopeName : CLR_Ch8.dll
      //...
      TypeDef #1 (02000002)
      -------------------------------------------------------
      //...
      TypeRef #1 (01000001)
      -------------------------------------------------------
      //...
      TypeSpec #1 (1b000001)
      -------------------------------------------------------
      //...
      MethodSpec #1 (2b000001)
      //...
      Signature #1 (0x11000001)
      -------------------------------------------------------
      //...
      Assembly
      -------------------------------------------------------
      //...
      CustomAttribute #1 (0c000001)
      -------------------------------------------------------
      CustomAttribute Type: 0a000001
      CustomAttributeName: System.Runtime.CompilerServices.ExtensionAttribute :: instance void .ctor()
      Length: 4
      Value : 01 00 00 00                                     >               <
      ctor args: ()
      //...
  • 因此,如果代碼調用了一個不存在的實例方法,編譯器就根據這個特性的存在與否,來快速掃瞄引用的程序集、靜態類、擴展方法

參考書目

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