C#筆記 – 委托

委托的使用
  • 聲明委托類型
    • delegate 返回值 委托字段名(參數列表)

    • delegate void VarName(string input);
  • 創建/使用一個與對應的委托簽名(參數/返回值完全匹配)一樣的方法
    • void 與 非void 的返回類型是不匹配的,原因在於 系統的 JIT(Just-In-Time)階段需要知道,在執行方法時返回值是否會留在棧上。
  • 創建委托實例
    •  創建的表達式形式取決於是靜態方法還是實例方法
      • 靜態方法:直接指定類型名

      VarName varName1; //Delegate
      varName1 = new VarName(StaticClass.method); //static method in delegate
      • 實例方法:需要先創建類型的實例,這個實例稱為「操作的目標」

        • 調用委托實例時,會為這個對象調用方法。

      VarName varName2; //Delegate
      Instance instance = new Instance();
      varName2 = new VarName(instance.method); //instance method in delegate
        • 如果委托實例本身不能被回收,委托實例會阻止它的操作目標被作為垃圾回收。這可能會造成內存泄漏。

          • 尤其是當操作目標的預期存活周期比委托實例本身要短,而如果委托實例本身無法被回收,就會導致操作目標的存活周期被延長。

  • 調用委托實例
    • 如果有一個委托的變量,可以直接將其視為方法本身
    • 委托相當於指定一些代碼在特定時間執行(如單撃一個按鈕後發生某事)
合并和刪除委托
  • 委托類(System.Delegate)中有兩個靜態方法

    • Combine(通過+=/+操作符調用)

    • Remove(通過-=/-操作符調用)

  • 委托是不易變的。與String類型相似,實例被創建後,有關它的一切就不能改變

    • Combine和Remove都會操作現有的實例並創建一個新實例,不會更改原始委托對象。

  • 調用委托時,所有操作將順序執行

    • 如果委托的簽名有一個非void的返回類型,則Invoke的返回值是最後一個操作的返回值

    • 假設現在有一個操作列表(a, b, c),操作b的過程中拋出一個異常,異常會立即傳播,並阻止執行其他後續操作。

委托的內部實現
  • 在非托管C/C++中,非成員函數的地址只是一個內存地址,不攜帶任何額外信息,不是類型安全的

    • 而.NET的委托則可視為「類型安全」的回調機制

    • 將方法綁定到委托時,C#和CLR允許引用類型的協變性和逆變性

      • 協變:方法能返回從委托類型的返回類型派生的一個類型

      • 逆變:方法的參數可以是委托類型的參數類型的基類

      • delegate object MyCallback(FileStream fs);
        class Program
        {        
           static void Main(string[] args)
          {
               CallBackMethods cm = new CallBackMethods();
               MyCallback callback = cm.SomeMethod;
          }        
        }
      • 只有引用類型才支持協變性和逆變性

  • 編譯器和CLR如何協同工作來實現委托

    • 以上面的委托聲明代碼為例:

      • delegate object MyCallback(FileStream fs);
      • 編譯器實際上會為這段代碼定義一個完整的類

        • 換成C#代碼的話,大概會是這樣的

          • private class MyCallback:System.MulticastDelegate
            {
               //構造器
               public MyCallback(Object @object, IntPtr method);
               
               public virtual void Invoke(FileStream value);
               
               public virtual IAsyncResult BeginInvoke(FileStream fs, AsyncCallback callback, Object @object);
               
               public virtual Object EndInvoke(IAsyncResult result);
            }
        • 元數據

          • TypeDef #1 (02000002)
            -------------------------------------------------------
            TypDefName: CLR_Ch17.MyCallback (02000002)
            Flags     : [NotPublic] [AutoLayout] [Class] [Sealed] [AnsiClass] (00000100)
            Extends   : 0100000C [TypeRef] System.MulticastDelegate
            Method #1 (06000001)
            -------------------------------------------------------
            MethodName: .ctor (06000001)
            Flags     : [Public] [HideBySig] [ReuseSlot] [SpecialName] [RTSpecialName] [.ctor] (00001886)
            RVA       : 0x00000000
            ImplFlags : [Runtime] [Managed] (00000003)
            CallCnvntn: [DEFAULT]
            hasThis
            ReturnType: Void
            2 Arguments
            Argument #1: Object
            Argument #2: I
            2 Parameters
            (1) ParamToken : (08000001) Name : object flags: [none] (00000000)
            (2) ParamToken : (08000002) Name : method flags: [none] (00000000)

            Method #2 (06000002)
            -------------------------------------------------------
            MethodName: Invoke (06000002)
            Flags     : [Public] [Virtual] [HideBySig] [NewSlot] (000001c6)
            RVA       : 0x00000000
            ImplFlags : [Runtime] [Managed] (00000003)
            CallCnvntn: [DEFAULT]
            hasThis
            ReturnType: Object
            1 Arguments
            Argument #1: Class System.IO.FileStream
            1 Parameters
            (1) ParamToken : (08000003) Name : fs flags: [none] (00000000)

            Method #3 (06000003)
            -------------------------------------------------------
            MethodName: BeginInvoke (06000003)
            Flags     : [Public] [Virtual] [HideBySig] [NewSlot] (000001c6)
            RVA       : 0x00000000
            ImplFlags : [Runtime] [Managed] (00000003)
            CallCnvntn: [DEFAULT]
            hasThis
            ReturnType: Class System.IAsyncResult
            3 Arguments
            Argument #1: Class System.IO.FileStream
            Argument #2: Class System.AsyncCallback
            Argument #3: Object
            3 Parameters
            (1) ParamToken : (08000004) Name : fs flags: [none] (00000000)
            (2) ParamToken : (08000005) Name : callback flags: [none] (00000000)
            (3) ParamToken : (08000006) Name : object flags: [none] (00000000)

            Method #4 (06000004)
            -------------------------------------------------------
            MethodName: EndInvoke (06000004)
            Flags     : [Public] [Virtual] [HideBySig] [NewSlot] (000001c6)
            RVA       : 0x00000000
            ImplFlags : [Runtime] [Managed] (00000003)
            CallCnvntn: [DEFAULT]
            hasThis
            ReturnType: Object
            1 Arguments
            Argument #1: Class System.IAsyncResult
            1 Parameters
            (1) ParamToken : (08000007) Name : result flags: [none] (00000000)
        • 該類一共有4個方法:

          • 構造方法

          • Invoke

          • BeginInvoke

          • EndInvoke

        • 訪問權限與委托的聲明一致

        • 由於委托實際上是一個類,因此能定義類的地方都能定義委托

    • 所有委托類型都派生自MulticastDelegate

      • MulticastDelegate派生自Delegate,Delegate再派生自Object

      • 這是C#的歷史遺留問題,FCL本該只有一個委托類

      • MulticastDelegate有三個最重要的私有字段

        • FieldTypeDescription
          _targetSystem.Object這個字段引用的是回調方法的操作對象;如果是靜態方法,該值為null
          _methodPtrSystem.IntPtr內部的整數值,CLR用它標識要回調的方法
          _invocationListSystem.Object通常為null。構造委托鏈時引用一個委托數組
      • 所有委托類型都有一個接受兩個參數的構造器

        • 參數

          • Object @object

          • IntPrt method

        • 當我們構造一個委托時,C#編譯器會分析源代碼來確定引用的對象和方法

          • 對象的引用會被傳給構造器的@object參數,並保存在_target中

          • 方法的標識值會被傳遞至構造器的method參數,並保存在_methodPtr中

            • 該值從MethodDef/MemberRef元數據中的token獲得

          • 每個委托對象都是一個包裝器,其中包裝了一個方法和調用該方法時操作的對象

            • CallBackMethods cm = new CallBackMethods();
              MyCallback myCallback = new MyCallback(cm.SomeMethod);
              MyCallback myStaticCallback = new MyCallback(CallBackMethods.SomeStaticMethod);
委托鏈
  • delegate void TestDelegate(int n);

    class Program
    {        
       static void Main(string[] args)
      {
           TestDelegate d_Base = null;
           TestDelegate d1 = new TestDelegate(cm.TestMethod_A);
           TestDelegate d2 = new TestDelegate(cm.TestMethod_B);
           TestDelegate d3 = new TestDelegate(cm.TestMethod_C);

           d_Base = (TestDelegate)Delegate.Combine(d_Base, d1);
           d_Base = (TestDelegate)Delegate.Combine(d_Base, d2);
           d_Base = (TestDelegate)Delegate.Combine(d_Base, d3);
      }        
    }
  • 首次執行Combine時:

    • d_Base = (TestDelegate)Delegate.Combine(d_Base, d1);
    • Combine方法發現試圖合併null和d1,直接返回d1的引用對象

  • 再次執行Combine時:

    • d_Base = (TestDelegate)Delegate.Combine(d_Base, d2);
    • Combine方法發現d_Base已引用了一個委托對象(d1),Combine會構造一個新的委托對象

      • 初始化其私有字段(具體的值不重要)

      • 重要的是_invocationList會被初始化為引用一個「委托對象數組」

        • 第一個元素被初始化為引用包裝了TestMethod_A方法的委托

        • 第二個元素被初始化為引用包裝了TestMethod_B方法的委托

      • d_Base被設為引用新建的委托對象

  • 最後一次執行Combine時:

    • d_Base = (TestDelegate)Delegate.Combine(d_Base, d3);
      • 再次執行上面第二次執行Combine的流程

      • 再新增一個委托對象並被d_Base所引用,d_Base的_invocationList引用一個包含了TestMethod_A/B/C的委托對象數組

      • 之前新增的委托和_invocationList(包含TestMethodA/B)將會進行GC

  • 示意圖:

  • 委托鏈的調用

    • class Program
      {        
         static void Main(string[] args)
        {
             //...
             d_Base(30);
        }        
      }

      public class CallBackMethods
      {
         public void TestMethod_A(int n)
        {
             Console.WriteLine($"A: {n}");
        }
         public void TestMethod_B(int n)
        {
             Console.WriteLine($"B: {n}");
        }
         public void TestMethod_C(int n)
        {
             Console.WriteLine($"C: {n}");
        }
      }
    • Output:

      • A: 30
        B: 30
        C: 30
    • 在d_Base引用的委托上調用Invoke時,會發現私有字段_invocationList != null,並執行一個循環遍歷數組中的所有元素,依次調用每個元素包裝的方法

    • 如果包裝的方法有返回值,可以在調用委托時接收其返回值,但只會接收到委托鏈中最後一個委托對象的返回值

      • delegate int TestRetDelegate(int n);

        static void Main(string[] args)
        {
           TestRetDelegate retD_Base = null;
           TestRetDelegate retD1 = new TestRetDelegate(cm.TestRetMethod_A);
           TestRetDelegate retD2 = new TestRetDelegate(cm.TestRetMethod_B);

           retD_Base = (TestRetDelegate)Delegate.Combine(retD_Base, retD1);            
           retD_Base = (TestRetDelegate)Delegate.Combine(retD_Base, retD2);

           int result = retD_Base(100);
           Console.WriteLine($"Result: {result}");
        }
        public class CallBackMethods
        {
           public int TestRetMethod_A(int n)
          {
               Console.WriteLine($"Ret A: {n}");
               return n * 100;
          }

           public int TestRetMethod_B(int n)
          {
               Console.WriteLine($"Ret B: {n}");
               return n / 2;
          }
        }
      • 在該例子中,委托鏈先調用TestRetMethod_A,再調用TestRetMethod_B,各傳遞(100)作為實參

        • TestRetMethod_A按理返回10000

        • TestRetMethod_B按理返回50

        • 最後result的輸出為50,也就是TestRetMethod_B的返回值

      • 如果需要獲取更多的控制,可以使用MulticastDelegate提供的實例方法:GetInvocationList,顯式調用鏈中的每一個委托

        • GetInvocationList內部重新構造了一個數組,元素依次指向_invocationList的每個委托,並返回該數組的引用

        • Delegate[] retDs = retD_Base.GetInvocationList();

          object result_A = retDs[0].Method.Invoke(cm, new object[] { 100 });
          Console.WriteLine($"Result_A: {result_A}"); //Output: 10000

          object result_B = retDs[1].DynamicInvoke(new object[] { 100 });
          Console.WriteLine($"Result_B: {result_B}"); //Output: 50
  • 移除委托

    • d_Base = (TestDelegate)Delegate.Remove(d_Base, d1);
    • Remove被調用時,會掃瞄第一個實參(d_Base)所引用的委托內部的_invocationList

      • 在裡面查找 _target字段和 _methodPtr字段與第二個參數匹配的委托,找到則移除

        • 移除後剩餘一個數據項,d_Base引用直接指向該剩餘的委托對象

        • 移除後剩餘多個數據項,新建一個不包含移除對象委托的_invocationList的委托對象,並讓d_Base引用該新對象

        • 移除後剩餘零個數據項,返回null

      • 如果有多於一個匹配項,每次Remove只會移除1個匹配項

委托與事件
  • 事件不是委托類型的字段,更像是一個委托類型的屬性

    • 我們對事件的操作只有 +=/-=和對其的Invoke

    • 因此事件更像對委托的一種保護機制,為委托提供了一個額外的封裝層

    using System;

    class Test
    {
       public event EventHandler MyEvent
      {
           //當我們在使用 +=/-=時,實際上是在調用 add 和 remove方法
           //我們所能做的只有「訂閱」和「取消訂閱」
           add //+=
          {
               Console.WriteLine ("add operation");
          }
           
           remove //-=
          {
               Console.WriteLine ("remove operation");
          }
      }      
       
       static void Main()
      {
           Test t = new Test();
           
           t.MyEvent += new EventHandler (t.DoNothing);
           t.MyEvent -= null;
      }
       
       void DoNothing (object sender, EventArgs e)
      {
      }
    }
  • 事件之所以容易被人當成委托,是因為C#提供了一個事件的shortcut,字段風格的聲明方式。

    • 在字段風格的事件聲明中,編譯器會將聲明轉換成一個具有默認add/remove實現的事件和一個私有的委托類型的字段。

      • 因此,類外的代碼只能看見事件。表面上似乎能調用一個事件,但實際上是調用存儲在字段中的委托實例。

public event VarName MyEvent; //事件
public delegate void VarName(string input); //委托
委托Lambda表達式
  • 表達式的類型并非委托類型,但它可以通過隱式/顯式轉換成一個委托實例

  • 匿名函數 = 匿名方法 + Lambda表達式

    • 兩者在很多情況下可以使用相同的轉換規則

  • Func<T1-16, TResult>

    • T1-16:可獲16的類型參數

    • TResult:返回類型參數

  • 使用 => 操作符,告訴編譯器我們正使用一個Lambda表達式

    • Func<string, int> returnLength;
      returnLength = (string text) => { return text.Length; }

      //單一表達式作為主體
      //該表達式的值是Lambda的結果
      //只指定表達式,不使用大括號,不使用return語句,也不添加分號
      returnLength = (string text) => text.Length;

      //隱式類型的參數列表
      //由編譯器去推斷,不需要顯式聲明(整個列表就是顯式/隱式)
      //如果有out/ref參數,就必須使用顯式類型
      returnLength = (text) => text.Length;

      //單一參數
      //如果只需要一個參數,可以省略括號
      returnLength = text => text.Length;
  • 高階(High-order)函數

    • Lambda表達式的主體本身包含另一個Lambda表達式

    • Lambda表達式的參數是另一個委托

    • 如:List的過濾/排序/操作

//C# Code
public class Film
{
   public string Name { get; set; }
   public int Year { get; set; }
}

static void Main()
{
   var films = new List<Film>
  {
       new Film{ Name = "The Wizard of Oz", Year = 1939 },
       new Film{ Name = "Jaws", Year = 1975 },
       new Film{ Name = "Singing in the Rain", Year = 1952 },
  };
   Action<Film> print = film => Console.WriteLine($"Name={film.Name}, Year={film.Year}");
   films.ForEach(print);
   films.FindAll(film => film.Year < 1960).ForEach(print);
   films.Sort((f1, f2) => f1.Name.CompareTo(f2.Name));
   films.ForEach(print);    
}
  • 編譯器看到lambda表達式,會自動為其創建一個匿名函數和委托類型變量

    • static void Main(string[] args)
      {
         LambdaTest(obj => Console.WriteLine(obj), 50);
      }
      public static void LambdaTest(TestDelegate d, int n)
      {
         d?.Invoke(n);
      }
    • 方法名字的生成不是固定的,本次生成為”<>c”

    • 委托類型變量名字的生成也不是固定的,本次生成為”<>9__0_0″

  • 代碼大概會被編譯成這個樣子

    • [CompilerGenerated]
      private static TestDelegate <>9__0_0; //自動生成的委托類型名
         
      static void Main(string[] args)
      {
         if(<>9__0_0 == null)
        {
             <>9__0_0 = new TestDelegate(<Main>b__0_0)
        }
         LambdaTest(<>9__0_0, 50);
      }

      [CompilerGenerated]
      private static void <Main>b__0_0(Object obj)
      {
         Console.WriteLine(obj);
      }
  • 另外,如果Lambda表達式中使用到了一些局部變量的話,C#也會為我們完成一些複雜的過程

    • int n = 50;
      static void Main(string[] args)
      {
         LambdaTest(obj => Console.WriteLine(obj), n);
      }
    • C#會定義一個輔助類

      • 該類為打算傳給回調代碼的每個值都定義一個字段

      • 回調代碼會定義成輔助類中的實例方法

      • 構造輔助類的實例,用局部變量的值來初始化輔助類裡的字段

      • 最後構造綁定到輔助對象/實例的委托對象

      • 示例:

//Origin CSharp
internal sealed class AClass
{
   public static void UsingLocalVariableInTheCallbackCode(int numToDo)
  {
       int[] squares = new int[numToDo];
       AutoResetEvent done = new AutoResetEvent(false);

for (int n = 0; n < squares.Length; n++)
      {
           ThreadPool.QueueUserWorkItem(
               obj => {
                   int num = (int)obj;
                   squares[num] = num * num;
                   if(Interlocked.Decrement(ref numToDo) == 0)
                  {
                       done.Set();
                  }
              }, n);                    
      }

done.WaitOne();

for
(int n = 0; n < squares.Length; n++)

      {
           Console.WriteLine($"Index :{n}, Square={squares[n]}");
      }
  }        
}
//Rewrite By CSharp
internal sealed class AClass
{
   public static void UsingLocalVariableInTheCallbackCode(int numToDo)
  {
       //Auto Added
       WaitCallback callback1 = null;
       
       //Support Class Instance
       <>c_DisplayClass2 class1 = new <>c_DisplayClass2();
       
       //Init Support class fields
       class1.numToDo = numToDo;
       class1.squares = new int[class1.numToDo];
       class1.done = new AutoResetEvent(false);
       
       for(int n = 0; n < class1.squares.Length; n++)
      {
           if(callback1 == null)
          {
               //新建的委托對象綁定到輔助對象及其匿名實例方法
               callback1 = new WaitCallback(
              class1.<UsingLocalVariablesInTheCallbackCode>b__0);
          }
           
           ThreadPool.QueueUserWorkItem(callback1, n);
      }

class1.done.WaitOne();

for (int n = 0; n < class1.squares.Length; n++)
      {
           Console.WriteLine($"Index :{n}, Square={class1.squares[n]}");
      }
  }        
}

//Support Class
[CompileGenerated]
private sealed class <>c_DisplayClass2:Object{
   public int[] squares;
   public int numToDo;
   public AutoResetEvent done;
   
   public <>c_DisplayClass2 { }
   
   public void <UsingLocalVariablesInTheCallbackCode>b__0(Object obj)
  {
       int num = (int)obj;
       squares[num] = num * num;
       if(Interlocked.Decrement(ref numToDo) == 0)
      {
           done.Set;
      }
  }
}
  • 無論是把Lambda表達式當作方法實參還是賦給委托實例,編譯器都會為Lambda表達式創建一個方法

    • 因為編譯器知道以後如果代碼再次被調用,可以重用委托實例,所以會把它緩存下來

委托與反射
  • 編譯時在對委托的所有必要信息存在未知時創建委托

    • System.Delegate.MethodInfo.CreateDelegate

    • 使用Delegate的實例方法DynamicInvoke進行調用

參考書目

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