.Neter所應該徹底了解的委托


 

本文將通過引出幾個問題來,並且通過例子來剖析C#中的委托以及用法,做拋磚引玉的作用

對於委托我發現大部分人都有以下問題,或者可能在面試中遇過這樣的:

  • 委托是不是相當於C/C++的函數指針?
  • 委托究竟是什么?
  • 委托究竟是用來干嘛的?
  • 委托跟匿名函數的區別?
  • 委托與事件的關系?


我們先來聲明和使用C++的函數指針:
代碼如下:

#include <iostream>
using namespace std; typedef int (*Foohandle)(int a,int b); int fooMenthod(int a, int (*foohandle1)(int a,int b)) //回調函數
{  return a + (*foohandle1)(2,3);//也可以寫成foohandle1(2,3)
} int add(int a,int b) {  return a + b; } int multiply(int a, int b) {  return a * b; } int main() { Foohandle foohandle = add;  int (*foohandle1)(int a, int b) = &add; cout << foohandle(2,3)<<endl; cout << foohandle1(2,3) << endl; cout << typeid(Foohandle).name() << endl; cout << typeid(foohandle).name()<<endl; cout << typeid(foohandle1).name() << endl; cout << fooMenthod(2, add)<<endl; cout << fooMenthod(2, multiply); }

輸出結果如下:

     

 

 

 

 

    在代碼中,我聲明定義了兩個函數add和multiply,然后用typedef方式聲明了函數指針,接着我分別將add賦值給Foohandle這種函數指針類型foohandle變量,然后用&add這種解地址的方式賦值給一個返回值為int,且帶有兩個參數的函數指針foohandle1,其中(*foohandle1)是函數名,最后我輸出發現它們類型和輸出都是一致的,再后面,我們定義了一個fooMenthod函數,返回值是int,且其中一個參數是函數指針,那么我再最后調用兩次,分別將add和multiply函數,賦值給它,這時候add和multiply就是fooMenthod函數的回調函數,且此時輸出結果會被兩個函數內部不同實現所影響
那么我們可以做個總結:

  • 首先函數指針就是一個內存地址,指向函數的入口內存地址
  • 當函數指針做一個函數的參數時,確實會起到一定解耦作用
  • 函數指針很明顯是類型不安全的

我們再來聲明和使用委托:

public delegate int Foohandle(int a, int b);
class Program
{
static void Main(string[] args)
{
 Foohandle foohandle = new Foohandle(add);
 Console.WriteLine(foohandle(2, 3));
 Console.WriteLine(foohandle.GetType().Name);
 Console.WriteLine(fooMenthod(2, add));
 Console.WriteLine(fooMenthod(2, multiply));
 Console.WriteLine($"foohandle所調用函數函數名:{foohandle.Method.Name}");
 Console.WriteLine($"foohandle所調用函數的返回值類型{foohandle.Method.ReturnType.ToString()}");
 Console.WriteLine("foohandle所調用函數參數類型以及參數名分別為:");
 Console.WriteLine($"Type:{foohandle.Method.GetParameters()[0].ParameterType},Name:{foohandle.Method.GetParameters()[0].Name}");
 Console.WriteLine($"Type:{foohandle.Method.GetParameters()[1].ParameterType},Name:{foohandle.Method.GetParameters()[1].Name}");
 Console.Read();
}

static int fooMenthod(int a, Foohandle foohandle) //傳給參數函數的就是回調函數
{
 return a + foohandle(2, 3);
}

static int add(int a, int b)
{
 return a + b;
}

static int multiply(int a, int b)
{
 return a * b;
}
}

輸出結果:

   

 

 

 

 

     

    很明顯,不管是聲明和使用方式,都和c++那邊一樣,就連輸出結果也差不多,但是很有意思的是,foohandle的類型是Foohandle,且我居然能從foohandle輸出所調函數的一切信息,包括函數名,返回值,參數類型和參數名,而且和c++那邊不同的是,我們沒有直接操作內存地址,好像看起來是安全的?那么Foohandle類型又是什么?

委托是啥?

先來個例子:

namespace DelegateSample
{

  public delegate void FooHandle(int value);
class Program { static void Main(string[] args) { FooHandle fooHandle = new FooHandle(multiply); fooHandle(3); Console.WriteLine($"fooHandle.Target:{fooHandle.Target},fooHandle.Method:{fooHandle.Method},fooHandle.InvocationListCount:{fooHandle.GetInvocationList().Count()}"); Console.WriteLine("-----------------------------------------------------------------------------------------------------------------------------------"); FooHandle fooHandle1 = new FooHandle(new Foo().Add); fooHandle1.Invoke(3); Console.WriteLine($"fooHandle1.Target:{fooHandle1.Target},fooHandle1.Method:{fooHandle1.Method},fooHandle1.InvocationListCount:{fooHandle1.GetInvocationList().Count()}"); Console.Read(); } static void multiply(int a) { Console.WriteLine(a*2); } } public class Foo { public void Add(int value) { Console.WriteLine(value + 2); } } }

我們看看輸出的結果:

     很明顯,這里是一個最簡單的委托聲明,實例化初始化一個委托對象,然后調用的最簡單的場景
     我們不關注輸出的第一行,很明顯,對象實例化后,可以訪問其中的三個公開public的函數成員,
分別是Target(object類型),Method(MethodInfo類型),而GetInvocationList函數是一個返回值為一個Delegate[]的無參函數
     在上面代碼,其實我還特地將委托FooHandle聲明在Program類外面,其實在這里我們已經知道委托是什么了,實例化對象,且能夠聲明在類外面,其實它本質就是一個類,我們通過反編譯來驗證:

 

 

 

 

 

 

大概是這樣,偽代碼如下:

public class FooHandle: MulticastDelegate {  public FooHandle(object @object,IntPtr menthod);//構造方法

 void Invoke(int value)//調用委托,編譯后公共語言運行時給delegate提供的特殊方法

 void EndInvoke(System.IAsyncResult asyncResult)// 編譯后公共語言運行時給MulticastDelegate提供的特殊方法  // 編譯后公共語言運行時給MulticastDelegate提供的特殊方法
 void BeginInvoke(int value,System.AsyncCallback callback, object obj) }

 

我們可以看編譯后FooHandle就是一個類,且繼承MulticastDelegate,且繼承鏈關系在msdn是這樣的:



   

   

    且我們發現上面公開的三個函數成員都來自於Delegate類,且編譯后生成了幾個公共運行時提供的特殊方法,Invoke方法我們很清楚,是來調用委托的,我們先來看看委托初始化后的情況,通過查看Delegate的源碼,我們發現Delegate有兩個構造函數:

1.委托對象初始化構造函數是實例函數:

[SecuritySafeCritical] protected Delegate(object target, string method) {  if (target == null) {  throw new ArgumentNullException("target"); }  if (method == null) {  throw new ArgumentNullException("method"); }  if (!BindToMethodName(target, (RuntimeType)target.GetType(), method, (DelegateBindingFlags)10)) {  throw new ArgumentException(Environment.GetResourceString("Arg_DlgtTargMeth")); } } 

 

2.委托對象初始化構造函數是靜態函數:

[SecuritySafeCritical] protected Delegate(Type target, string method) {  if (target == null) {  throw new ArgumentNullException("target"); }  if (target.IsGenericType && target.ContainsGenericParameters) {  throw new ArgumentException(Environment.GetResourceString("Arg_UnboundGenParam"), "target"); }  if (method == null) {  throw new ArgumentNullException("method"); } RuntimeType runtimeType = target as RuntimeType;  if (runtimeType == null) {  throw new ArgumentException(Environment.GetResourceString("Argument_MustBeRuntimeType"), "target"); } BindToMethodName(null, runtimeType, method, (DelegateBindingFlags)37); }

最后共同調用的方法:

//調用CLR的內部代碼
[MethodImpl(MethodImplOptions.InternalCall)] [SecurityCritical] private extern bool BindToMethodName(object target, RuntimeType methodType, string method, DelegateBindingFlags flags);

    雖然我們看不到BindToMethodName方法的實現,已經很明顯了,委托對象初始化構造函數是靜態函數傳參進去BindToMethodName的第一個object的target參數為null,那我們大概把之前的偽代碼的構造函數這么實現了:

偽代碼部分:

internal object _target//目標對象;
internal IntPtr _methodPtr//目標方法;
internal IntPtr _methodPtrAux//用來判斷Target是否為空; //foolHandle的構造方法實現:
public FooHandle(object @object,IntPtr menthod) { _methodPtr=menthod;//multiply
  _methodPtrAux=1;//只要不等於nul
 } //foolHandle1的構造方法實現:
public FooHandle(object @object,IntPtr menthod) { _methodPtr=menthod//Add
  _methodPtrAux=0//為null
  _target=foo; }

 

Delegate Target屬性源代碼部分:

[__DynamicallyInvokable] public object Target { [__DynamicallyInvokable]  get {  return GetTarget(); } } [SecuritySafeCritical] internal virtual object GetTarget() {  if (!_methodPtrAux.IsNull()) {  return null; }  return _target; }

    而獲取Method的方法就不展開了,就是通過反射來獲取,那我們已經知道Target和Method屬性究竟是怎么回事了,我們還發現沒講到GetInvocationList方法是怎么回事?我們知道委托是支持多播委托的,也就是大概這樣,修改上述代碼為:

namespace DelegateSample {  public delegate void FooHandle(int value);
class Program { static void Main(string[] args) { FooHandle fooHandle = new FooHandle(multiply); fooHandle(3); Console.WriteLine($"fooHandle.Target:{fooHandle.Target},fooHandle.Method:{fooHandle.Method},fooHandle.InvocationListCount:{fooHandle.GetInvocationList().Count()}"); Console.WriteLine("----------------------------------------------------------------------------------------------------------------"); FooHandle fooHandle1 = new FooHandle(new Foo().Add); fooHandle1.Invoke(3); Console.WriteLine($"fooHandle1.Target:{fooHandle1.Target},fooHandle1.Method:{fooHandle1.Method},fooHandle1.InvocationListCount:{fooHandle1.GetInvocationList().Count()}"); Console.WriteLine(); Console.WriteLine("--------------------------------------------------新增代碼------------------------------------------------------"); FooHandle fooHandle2 = new FooHandle(new Program().Minus); Console.WriteLine($"fooHandle2.Target:{fooHandle2.Target},fooHandle1.Method:{fooHandle2.Method},fooHandle1.InvocationListCount:{fooHandle2.GetInvocationList().Count()}"); fooHandle2(2); Console.WriteLine("----------------------------------------------------------------------------------------------------------------"); FooHandle fooHandle3 = null; fooHandle3 += fooHandle; fooHandle3 =(FooHandle)Delegate.Combine(fooHandle3,fooHandle1);//相當於fooHandle3+=fooHandle1; fooHandle3 += new Program().Minus; Console.WriteLine($"fooHandle3.Target:{fooHandle3.Target},fooHandle3.Method:{fooHandle3.Method},fooHandle3.InvocationListCount:{fooHandle3.GetInvocationList().Count()}"); fooHandle3(2); foreach (var result in fooHandle3.GetInvocationList()) { Console.WriteLine($"result.Target:{result.Target},result.Method:{result.Method},result.InvocationListCount:{result.GetInvocationList().Count()}"); } Console.Read(); } private void Minus(int a) { Console.WriteLine(a-1); } static void multiply(int a) { Console.WriteLine(a * 2); } } public class Foo { public void Add(int value) { Console.WriteLine(value + 2); } } }

輸出結果是:

    上面新增的代碼,我聲明了一個新的委托變量fooHandle3初始化為null,接着分別用三種不同的方式將委托或者函數加給fooHandle,之后輸出后相當於分別按序調用輸出了三個方法,而我們遍歷其中的fooHandle3.GetInvocationList()委托數組,輸出的也確實三個方法,但是注意到了沒,我在fooHandle3 += new Program().Minus這段確實沒有聲明一個委托變量,我們可以注意到其中的(FooHandle)Delegate.Combine(fooHandle3,fooHandle1)這句,Combine很明顯是需要兩個委托變量的,查看編譯后的代碼我們可以得知到底發生了啥?
Il關鍵代碼如下:

//fooHandle3 += fooHandle
IL_00f7: call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate, class [mscorlib]System.Delegate) IL_00fc: castclass DelegateSample.FooHandle IL_0101: stloc.3 IL_0102: ldloc.3 IL_0103: ldloc.1
//fooHandle3 =(FooHandle)Delegate.Combine(fooHandle3,fooHandle1)
IL_0104: call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate, class [mscorlib]System.Delegate) IL_0109: castclass DelegateSample.FooHandle IL_010e: stloc.3 IL_010f: ldloc.3
//new Program()
IL_0110: newobj instance void DelegateSample.Program::.ctor() IL_0115: ldftn instance void DelegateSample.Program::Minus(int32) //new FooHandle()新增了一個FooHandle委托變量
IL_011b: newobj instance void DelegateSample.FooHandle::.ctor(object, native int) //fooHandle3 += new Program().Minus 
IL_0120: call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate, class [mscorlib]System.Delegate)

     也就是三種不同方式都會被翻譯為Combine方法,如果是直接+=函數這種情況,后台也會new一個委托變量,將方法賦值給該變量再加到fooHandle3,那么我們可以知道,最關鍵的核心代碼就應該是Delegate.combine這個靜態方法了,我們來看看源碼是怎么回事:
Delegate類的:

[__DynamicallyInvokable] public static Delegate Combine(Delegate a, Delegate b) {  if ((object)a == null) {  return b; }  return a.CombineImpl(b); } protected virtual Delegate CombineImpl(Delegate d) {  throw new MulticastNotSupportedException(Environment.GetResourceString("Multicast_Combine")); }

 

MulticastDelegate類的:

[SecurityCritical] private object _invocationList;//委托鏈表
 [SecurityCritical] private IntPtr _invocationCount; [SecuritySafeCritical] protected sealed override Delegate CombineImpl(Delegate follow) {  if ((object)follow == null) {  return this; }  if (!Delegate.InternalEqualTypes(this, follow)) {  throw new ArgumentException(Environment.GetResourceString("Arg_DlgtTypeMis")); } MulticastDelegate multicastDelegate = (MulticastDelegate)follow;  int num = 1;  object[] array = multicastDelegate._invocationList as object[];  if (array != null) { num = (int)multicastDelegate._invocationCount; }  object[] array2 = _invocationList as object[];  int num2;  object[] array3;  if (array2 == null) { num2 = 1 + num; array3 = new object[num2]; array3[0] = this;  if (array == null) { array3[1] = multicastDelegate; }  else {  for (int i = 0; i < num; i++) { array3[1 + i] = array[i]; } }  return NewMulticastDelegate(array3, num2); }  int num3 = (int)_invocationCount; num2 = num3 + num; array3 = null;  if (num2 <= array2.Length) { array3 = array2;  if (array == null) {  if (!TrySetSlot(array3, num3, multicastDelegate)) { array3 = null; } }  else {  for (int j = 0; j < num; j++) {  if (!TrySetSlot(array3, num3 + j, array[j])) { array3 = null;  break; } } } }  if (array3 == null) {  int num4;  for (num4 = array2.Length; num4 < num2; num4 *= 2) { } array3 = new object[num4];  for (int k = 0; k < num3; k++) { array3[k] = array2[k]; }  if (array == null) { array3[num3] = multicastDelegate; }  else {  for (int l = 0; l < num; l++) { array3[num3 + l] = array[l]; } } }  return NewMulticastDelegate(array3, num2, thisIsMultiCastAlready: true); }

 

GetInvocationList方法的實現:

//Delgate類的
public virtual Delegate[] GetInvocationList() {  return new Delegate[1] {  this }; } //MulticastDelegate類的
public sealed override Delegate[] GetInvocationList() {  object[] array = _invocationList as object[]; Delegate[] array2;  if (array == null) { array2 = new Delegate[1] {  this }; }  else {  int num = (int)_invocationCount; array2 = new Delegate[num];  for (int i = 0; i < num; i++) { array2[i] = (Delegate)array[i]; } }  return array2; }

 

    其實我們看到這里,就可以知道其中的一個最主要就是_invocationList變量,也就是當調用Combine的時候,會判斷左邊委托變量是否為空,如果為空,會返回右邊的委托變量,不為空就會調用CombineImpl方法,以上面那個例子來說fooHandle3_invocationList存儲着所有附加到委托變量,包含對象本身,也就是為啥遍歷fooHandle3.GetInvocationList,輸出了三個附加到fooHandle3變量的委托變量,這里例子fooHandle3初始化為null,還有意思的是fooHandle3的Targt和Menthod屬性是最后附加的那個委托變量的Target和Menthod,而當委托由返回值,也同理返回最后一個函數的返回值,那么fooHandle3大概的結構如下圖:

 

     我們到現在只用到+=,其實-=就是調用其Delegate.Remove方法,跟Combine方法作用相反,具體就不多概述
看到這里我們終於可以回答一開頭拋出的幾個問題?

  • 委托是不是相當於C/C++的函數指針?

          很明顯,不是的,從數據結構來說,c++函數指針表示一塊指向函數的內存地址,它其實和直接寫函數名沒啥區別,因為我們調用函數時的函數名,也是函數入口地址,而委托卻是個類,是一塊托管內存,使用Invoke后它就會被clr釋放了,它的函數成員能夠存儲所調函數的所有信息,這是函數指針沒做到的,但是在某些特殊情況下,C++的函數指針就和委托一樣,有興趣的朋友可以去看下p/invoke方面知識

  • 委托是什么?

          委托本質是類,且支持多播委托的本質是維護一個私有的_invocationList委托鏈對象,+=和-=都是調用其靜態方法Combine和Remove

  • 委托是用來做啥的?

          委托和c++函數指針一樣,都可以作為函數中轉器,在調用者和被調用者中起解耦作用,可作為函數的參數,當回調函數

  • 委托跟匿名函數的區別?

         我們先來聲明和使用匿名函數:

public delegate int Foohandle(int a, int b);

Foohandle foohandle = delegate (int a, int b) { return a + b; };//匿名方法方式
Foohandle foohandle1= (a, b)=> a + b;//Lambda 表達式方式

foohandle.Invoke(2,2);//輸出4
foohandle1.Invoke(2,2);//輸出4

 

我們來看下msdn是怎么定義匿名函數的:

 

很明顯,匿名函數只是個表達式,可以用來初始化委托的,而委托是個類,其實通過查看IL,后台都會實例化一個新的委托對象,並把該表達式作為函數賦給它

  • 委托與事件的關系?

 同樣的我們來聲明和使用事件:

public class Foo
{
   public delegate void Foohandel(int a, int b);

   public event Foohandel foohandle;

   public Foo()
   {
      foohandle = new Foohandel(add);
      foohandle(2,2);//在Foo里面可以直接調用事件
      Console.WriteLine($"{foohandle.Target},{foohandle.Method}");
   }

   public void excute(int a,int b)//公開給外部類調用事件的函數
   {
      foohandle?.Invoke(a,b);
   }

   private void add(int a, int b)
   {
      Console.WriteLine(a + b); 
   }
}

class Program
{
   static void Main(string[] args)
   {
      Foo foo = new Foo();
      //foo.foohandle = new Foo.Foohandel(multiply);編譯不過,提示foo.foohandle只能出現再+=和-=左邊
      foo.foohandle +=new Foo.Foohandel(multiply);
      foo.excute(2, 2); 
      Console.Read();
   }

   static void multiply(int a,int b)
   {
      Console.WriteLine(a * b); 
   }
}

 

輸出結果:

4
EventSample.Foo,Void add(Int32, Int32)
4
4

 

     我們發現,在Foo類里面,事件foohandle就是相當於委托,但是在外部,我們再program的main函數訪問它時候,我們發現foohandle只能做+=或者-=,也不能訪問其函數成員Target和Menthod,而我們只能通過調用excute函數去調用,這時候我們可以知道,Event其實是基於委托的,在內部類相當於委托,在外部就只能有委托的多播功能,其余都不能訪問,其實我們想到,屬性是不是這樣。。。有興趣的朋友可以去了解事件的原理,也是很有趣


最后的最后,我們還要談下委托的一個功能:

委托的參數逆變和返回值的協變

由於委托也支持泛型委托,因此我們可以看看微軟定義好的

public delegate void Action<in T>(T obj);//其中in表示逆變
public delegate TResult Func<out TResult>();//其中out表示協變

class Program
{
    static Action<object> action;
    static Func<string> func;
    static void Main(string[] args)
    {
       action = (object a) => { Console.WriteLine(a.ToString()); };
       Action<string> action1 = action;//參數逆變
       action("Hello!");


       func = () => { return "I am Func"; };
       Func<object> func1 = func;//返回值協變
       Console.WriteLine(func1()); 
       Console.ReadLine();
    }

}

 

輸出結果:

Hello!
I am Func

想要了解更深的朋友可以去了解泛型的協變和逆變,在這里就不深入探討了

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM