本文将通过引出几个问题来,并且通过例子来剖析C#中的委托以及用法,做抛砖引玉的作用

对于委托我发现大部分人都有以下问题,或者可能在面试中遇过这样的:

  • 委托是不是相当于C/C++的函数指针?
  • 委托究竟是什么?
  • 委托究竟是用来干嘛的?
  • 委托跟匿名函数的区别?
  • 委托与事件的关系?

我们先来声明和使用C++的函数指针:
代码如下:

  1. #include <iostream>
  2. using namespace std;
  3. typedef int (*Foohandle)(int a,int b);
  4. int fooMenthod(int a, int (*foohandle1)(int a,int b)) //回调函数
  5. {
  6. return a + (*foohandle1)(2,3);//也可以写成foohandle1(2,3)
  7. }
  8. int add(int a,int b) {
  9. return a + b;
  10. }
  11. int multiply(int a, int b) {
  12. return a * b;
  13. }
  14. int main()
  15. {
  16. Foohandle foohandle = add;
  17. int (*foohandle1)(int a, int b) = &add;
  18. cout << foohandle(2,3)<<endl;
  19. cout << foohandle1(2,3) << endl;
  20. cout << typeid(Foohandle).name() << endl;
  21. cout << typeid(foohandle).name()<<endl;
  22. cout << typeid(foohandle1).name() << endl;
  23. cout << fooMenthod(2, add)<<endl;
  24. cout << fooMenthod(2, multiply);
  25. }

输出结果如下:

     

 

 

 

 

    在代码中,我声明定义了两个函数add和multiply,然后用typedef方式声明了函数指针,接着我分别将add赋值给Foohandle这种函数指针类型foohandle变量,然后用&add这种解地址的方式赋值给一个返回值为int,且带有两个参数的函数指针foohandle1,其中(*foohandle1)是函数名,最后我输出发现它们类型和输出都是一致的,再后面,我们定义了一个fooMenthod函数,返回值是int,且其中一个参数是函数指针,那么我再最后调用两次,分别将add和multiply函数,赋值给它,这时候add和multiply就是fooMenthod函数的回调函数,且此时输出结果会被两个函数内部不同实现所影响
那么我们可以做个总结:

  • 首先函数指针就是一个内存地址,指向函数的入口内存地址
  • 当函数指针做一个函数的参数时,确实会起到一定解耦作用
  • 函数指针很明显是类型不安全的

我们再来声明和使用委托:

  1. public delegate int Foohandle(int a, int b);
  2. class Program
  3. {
  4. static void Main(string[] args)
  5. {
  6. Foohandle foohandle = new Foohandle(add);
  7. Console.WriteLine(foohandle(2, 3));
  8. Console.WriteLine(foohandle.GetType().Name);
  9. Console.WriteLine(fooMenthod(2, add));
  10. Console.WriteLine(fooMenthod(2, multiply));
  11. Console.WriteLine($"foohandle所调用函数函数名:{foohandle.Method.Name}");
  12. Console.WriteLine($"foohandle所调用函数的返回值类型{foohandle.Method.ReturnType.ToString()}");
  13. Console.WriteLine("foohandle所调用函数参数类型以及参数名分别为:");
  14. Console.WriteLine($"Type:{foohandle.Method.GetParameters()[0].ParameterType},Name:{foohandle.Method.GetParameters()[0].Name}");
  15. Console.WriteLine($"Type:{foohandle.Method.GetParameters()[1].ParameterType},Name:{foohandle.Method.GetParameters()[1].Name}");
  16. Console.Read();
  17. }
  18. static int fooMenthod(int a, Foohandle foohandle) //传给参数函数的就是回调函数
  19. {
  20. return a + foohandle(2, 3);
  21. }
  22. static int add(int a, int b)
  23. {
  24. return a + b;
  25. }
  26. static int multiply(int a, int b)
  27. {
  28. return a * b;
  29. }
  30. }

输出结果:

   

 

 

 

 

     

    很明显,不管是声明和使用方式,都和c++那边一样,就连输出结果也差不多,但是很有意思的是,foohandle的类型是Foohandle,且我居然能从foohandle输出所调函数的一切信息,包括函数名,返回值,参数类型和参数名,而且和c++那边不同的是,我们没有直接操作内存地址,好像看起来是安全的?那么Foohandle类型又是什么?

先来个例子:

  1. namespace DelegateSample
  2. {
  3. public delegate void FooHandle(int value);
    class Program
  4. {
  5. static void Main(string[] args)
  6. {
  7. FooHandle fooHandle = new FooHandle(multiply);
  8. fooHandle(3);
  9. Console.WriteLine($"fooHandle.Target:{fooHandle.Target},fooHandle.Method:{fooHandle.Method},fooHandle.InvocationListCount:{fooHandle.GetInvocationList().Count()}");
  10. Console.WriteLine("-----------------------------------------------------------------------------------------------------------------------------------");
  11. FooHandle fooHandle1 = new FooHandle(new Foo().Add);
  12. fooHandle1.Invoke(3);
  13. Console.WriteLine($"fooHandle1.Target:{fooHandle1.Target},fooHandle1.Method:{fooHandle1.Method},fooHandle1.InvocationListCount:{fooHandle1.GetInvocationList().Count()}");
  14. Console.Read();
  15. }
  16. static void multiply(int a)
  17. {
  18. Console.WriteLine(a*2);
  19. }
  20. }
  21. public class Foo
  22. {
  23. public void Add(int value)
  24. {
  25. Console.WriteLine(value + 2);
  26. }
  27. }
  28. }

我们看看输出的结果:

     很明显,这里是一个最简单的委托声明,实例化初始化一个委托对象,然后调用的最简单的场景
     我们不关注输出的第一行,很明显,对象实例化后,可以访问其中的三个公开public的函数成员,
分别是Target(object类型),Method(MethodInfo类型),而GetInvocationList函数是一个返回值为一个Delegate[]的无参函数
     在上面代码,其实我还特地将委托FooHandle声明在Program类外面,其实在这里我们已经知道委托是什么了,实例化对象,且能够声明在类外面,其实它本质就是一个类,我们通过反编译来验证:

 

 

 

 

 

 

大概是这样,伪代码如下:

  1. public class FooHandle: MulticastDelegate
  2. {
  3. public FooHandle(object @object,IntPtr menthod);//构造方法
  4. void Invoke(int value)//调用委托,编译后公共语言运行时给delegate提供的特殊方法
  5. void EndInvoke(System.IAsyncResult asyncResult)// 编译后公共语言运行时给MulticastDelegate提供的特殊方法
  6. // 编译后公共语言运行时给MulticastDelegate提供的特殊方法
  7. void BeginInvoke(int value,System.AsyncCallback callback, object obj)
  8. }

 

我们可以看编译后FooHandle就是一个类,且继承MulticastDelegate,且继承链关系在msdn是这样的:

   

   

    且我们发现上面公开的三个函数成员都来自于Delegate类,且编译后生成了几个公共运行时提供的特殊方法,Invoke方法我们很清楚,是来调用委托的,我们先来看看委托初始化后的情况,通过查看Delegate的源码,我们发现Delegate有两个构造函数:

1.委托对象初始化构造函数是实例函数:

  1. [SecuritySafeCritical]
  2. protected Delegate(object target, string method)
  3. {
  4. if (target == null)
  5. {
  6. throw new ArgumentNullException("target");
  7. }
  8. if (method == null)
  9. {
  10. throw new ArgumentNullException("method");
  11. }
  12. if (!BindToMethodName(target, (RuntimeType)target.GetType(), method, (DelegateBindingFlags)10))
  13. {
  14. throw new ArgumentException(Environment.GetResourceString("Arg_DlgtTargMeth"));
  15. }
  16. }

 

2.委托对象初始化构造函数是静态函数:

  1. [SecuritySafeCritical]
  2. protected Delegate(Type target, string method)
  3. {
  4. if (target == null)
  5. {
  6. throw new ArgumentNullException("target");
  7. }
  8. if (target.IsGenericType && target.ContainsGenericParameters)
  9. {
  10. throw new ArgumentException(Environment.GetResourceString("Arg_UnboundGenParam"), "target");
  11. }
  12. if (method == null)
  13. {
  14. throw new ArgumentNullException("method");
  15. }
  16. RuntimeType runtimeType = target as RuntimeType;
  17. if (runtimeType == null)
  18. {
  19. throw new ArgumentException(Environment.GetResourceString("Argument_MustBeRuntimeType"), "target");
  20. }
  21. BindToMethodName(null, runtimeType, method, (DelegateBindingFlags)37);
  22. }

最后共同调用的方法:

  1. //调用CLR的内部代码
  2. [MethodImpl(MethodImplOptions.InternalCall)]
  3. [SecurityCritical]
  4. private extern bool BindToMethodName(object target, RuntimeType methodType, string method, DelegateBindingFlags flags);

    虽然我们看不到BindToMethodName方法的实现,已经很明显了,委托对象初始化构造函数是静态函数传参进去BindToMethodName的第一个object的target参数为null,那我们大概把之前的伪代码的构造函数这么实现了:

伪代码部分:

  1. internal object _target//目标对象;
  2. internal IntPtr _methodPtr//目标方法;
  3. internal IntPtr _methodPtrAux//用来判断Target是否为空;
  4. //foolHandle的构造方法实现:
  5. public FooHandle(object @object,IntPtr menthod)
  6. {
  7. _methodPtr=menthod;//multiply
  8. _methodPtrAux=1;//只要不等于nul
  9. }
  10. //foolHandle1的构造方法实现:
  11. public FooHandle(object @object,IntPtr menthod)
  12. {
  13. _methodPtr=menthod//Add
  14. _methodPtrAux=0//为null
  15. _target=foo;
  16. }

 

Delegate Target属性源代码部分:

  1. [__DynamicallyInvokable]
  2. public object Target
  3. {
  4. [__DynamicallyInvokable]
  5. get
  6. {
  7. return GetTarget();
  8. }
  9. }
  10. [SecuritySafeCritical]
  11. internal virtual object GetTarget()
  12. {
  13. if (!_methodPtrAux.IsNull())
  14. {
  15. return null;
  16. }
  17. return _target;
  18. }

    而获取Method的方法就不展开了,就是通过反射来获取,那我们已经知道Target和Method属性究竟是怎么回事了,我们还发现没讲到GetInvocationList方法是怎么回事?我们知道委托是支持多播委托的,也就是大概这样,修改上述代码为:

  1. namespace DelegateSample
  2. {
  3. public delegate void FooHandle(int value);
    class Program
  4. {
  5. static void Main(string[] args)
  6. {
  7. FooHandle fooHandle = new FooHandle(multiply);
  8. fooHandle(3);
  9. Console.WriteLine($"fooHandle.Target:{fooHandle.Target},fooHandle.Method:{fooHandle.Method},fooHandle.InvocationListCount:{fooHandle.GetInvocationList().Count()}");
  10. Console.WriteLine("----------------------------------------------------------------------------------------------------------------");
  11. FooHandle fooHandle1 = new FooHandle(new Foo().Add);
  12. fooHandle1.Invoke(3);
  13. Console.WriteLine($"fooHandle1.Target:{fooHandle1.Target},fooHandle1.Method:{fooHandle1.Method},fooHandle1.InvocationListCount:{fooHandle1.GetInvocationList().Count()}");
  14. Console.WriteLine();
  15. Console.WriteLine("--------------------------------------------------新增代码------------------------------------------------------");
  16. FooHandle fooHandle2 = new FooHandle(new Program().Minus);
  17. Console.WriteLine($"fooHandle2.Target:{fooHandle2.Target},fooHandle1.Method:{fooHandle2.Method},fooHandle1.InvocationListCount:{fooHandle2.GetInvocationList().Count()}");
  18. fooHandle2(2);
  19. Console.WriteLine("----------------------------------------------------------------------------------------------------------------");
  20. FooHandle fooHandle3 = null;
  21. fooHandle3 += fooHandle;
  22. fooHandle3 =(FooHandle)Delegate.Combine(fooHandle3,fooHandle1);//相当于fooHandle3+=fooHandle1;
  23. fooHandle3 += new Program().Minus;
  24. Console.WriteLine($"fooHandle3.Target:{fooHandle3.Target},fooHandle3.Method:{fooHandle3.Method},fooHandle3.InvocationListCount:{fooHandle3.GetInvocationList().Count()}");
  25. fooHandle3(2);
  26. foreach (var result in fooHandle3.GetInvocationList())
  27. {
  28. Console.WriteLine($"result.Target:{result.Target},result.Method:{result.Method},result.InvocationListCount:{result.GetInvocationList().Count()}");
  29. }
  30. Console.Read();
  31. }
  32. private void Minus(int a)
  33. {
  34. Console.WriteLine(a-1);
  35. }
  36. static void multiply(int a)
  37. {
  38. Console.WriteLine(a * 2);
  39. }
  40. }
  41. public class Foo
  42. {
  43. public void Add(int value)
  44. {
  45. Console.WriteLine(value + 2);
  46. }
  47. }
  48. }

输出结果是:

    上面新增的代码,我声明了一个新的委托变量fooHandle3初始化为null,接着分别用三种不同的方式将委托或者函数加给fooHandle,之后输出后相当于分别按序调用输出了三个方法,而我们遍历其中的fooHandle3.GetInvocationList()委托数组,输出的也确实三个方法,但是注意到了没,我在fooHandle3 += new Program().Minus这段确实没有声明一个委托变量,我们可以注意到其中的(FooHandle)Delegate.Combine(fooHandle3,fooHandle1)这句,Combine很明显是需要两个委托变量的,查看编译后的代码我们可以得知到底发生了啥?
Il关键代码如下:

  1. //fooHandle3 += fooHandle
  2. IL_00f7: call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate,
  3. class [mscorlib]System.Delegate)
  4. IL_00fc: castclass DelegateSample.FooHandle
  5. IL_0101: stloc.3
  6. IL_0102: ldloc.3
  7. IL_0103: ldloc.1
  8. //fooHandle3 =(FooHandle)Delegate.Combine(fooHandle3,fooHandle1)
  9. IL_0104: call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate,
  10. class [mscorlib]System.Delegate)
  11. IL_0109: castclass DelegateSample.FooHandle
  12. IL_010e: stloc.3
  13. IL_010f: ldloc.3
  14. //new Program()
  15. IL_0110: newobj instance void DelegateSample.Program::.ctor()
  16. IL_0115: ldftn instance void DelegateSample.Program::Minus(int32)
  17. //new FooHandle()新增了一个FooHandle委托变量
  18. IL_011b: newobj instance void DelegateSample.FooHandle::.ctor(object,
  19. native int)
  20. //fooHandle3 += new Program().Minus
  21. IL_0120: call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate,
  22. class [mscorlib]System.Delegate)

     也就是三种不同方式都会被翻译为Combine方法,如果是直接<b>+=函数</b>这种情况,后台也会new一个委托变量,将方法赋值给该变量再加到fooHandle3,那么我们可以知道,最关键的核心代码就应该是Delegate.combine这个静态方法了,我们来看看源码是怎么回事:
Delegate类的:

  1. [__DynamicallyInvokable]
  2. public static Delegate Combine(Delegate a, Delegate b)
  3. {
  4. if ((object)a == null)
  5. {
  6. return b;
  7. }
  8. return a.CombineImpl(b);
  9. }
  10. protected virtual Delegate CombineImpl(Delegate d)
  11. {
  12. throw new MulticastNotSupportedException(Environment.GetResourceString("Multicast_Combine"));
  13. }

 

MulticastDelegate类的:

  1. [SecurityCritical]
  2. private object _invocationList;//委托链表
  3. [SecurityCritical]
  4. private IntPtr _invocationCount;
  5. [SecuritySafeCritical]
  6. protected sealed override Delegate CombineImpl(Delegate follow)
  7. {
  8. if ((object)follow == null)
  9. {
  10. return this;
  11. }
  12. if (!Delegate.InternalEqualTypes(this, follow))
  13. {
  14. throw new ArgumentException(Environment.GetResourceString("Arg_DlgtTypeMis"));
  15. }
  16. MulticastDelegate multicastDelegate = (MulticastDelegate)follow;
  17. int num = 1;
  18. object[] array = multicastDelegate._invocationList as object[];
  19. if (array != null)
  20. {
  21. num = (int)multicastDelegate._invocationCount;
  22. }
  23. object[] array2 = _invocationList as object[];
  24. int num2;
  25. object[] array3;
  26. if (array2 == null)
  27. {
  28. num2 = 1 + num;
  29. array3 = new object[num2];
  30. array3[0] = this;
  31. if (array == null)
  32. {
  33. array3[1] = multicastDelegate;
  34. }
  35. else
  36. {
  37. for (int i = 0; i < num; i++)
  38. {
  39. array3[1 + i] = array[i];
  40. }
  41. }
  42. return NewMulticastDelegate(array3, num2);
  43. }
  44. int num3 = (int)_invocationCount;
  45. num2 = num3 + num;
  46. array3 = null;
  47. if (num2 <= array2.Length)
  48. {
  49. array3 = array2;
  50. if (array == null)
  51. {
  52. if (!TrySetSlot(array3, num3, multicastDelegate))
  53. {
  54. array3 = null;
  55. }
  56. }
  57. else
  58. {
  59. for (int j = 0; j < num; j++)
  60. {
  61. if (!TrySetSlot(array3, num3 + j, array[j]))
  62. {
  63. array3 = null;
  64. break;
  65. }
  66. }
  67. }
  68. }
  69. if (array3 == null)
  70. {
  71. int num4;
  72. for (num4 = array2.Length; num4 < num2; num4 *= 2)
  73. {
  74. }
  75. array3 = new object[num4];
  76. for (int k = 0; k < num3; k++)
  77. {
  78. array3[k] = array2[k];
  79. }
  80. if (array == null)
  81. {
  82. array3[num3] = multicastDelegate;
  83. }
  84. else
  85. {
  86. for (int l = 0; l < num; l++)
  87. {
  88. array3[num3 + l] = array[l];
  89. }
  90. }
  91. }
  92. return NewMulticastDelegate(array3, num2, thisIsMultiCastAlready: true);
  93. }

 

GetInvocationList方法的实现:

  1. //Delgate类的
  2. public virtual Delegate[] GetInvocationList()
  3. {
  4. return new Delegate[1]
  5. {
  6. this
  7. };
  8. }
  9. //MulticastDelegate类的
  10. public sealed override Delegate[] GetInvocationList()
  11. {
  12. object[] array = _invocationList as object[];
  13. Delegate[] array2;
  14. if (array == null)
  15. {
  16. array2 = new Delegate[1]
  17. {
  18. this
  19. };
  20. }
  21. else
  22. {
  23. int num = (int)_invocationCount;
  24. array2 = new Delegate[num];
  25. for (int i = 0; i < num; i++)
  26. {
  27. array2[i] = (Delegate)array[i];
  28. }
  29. }
  30. return array2;
  31. }

 

    其实我们看到这里,就可以知道其中的一个最主要就是_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++函数指针一样,都可以作为函数中转器,在调用者和被调用者中起解耦作用,可作为函数的参数,当回调函数

  • 委托跟匿名函数的区别?

         我们先来声明和使用匿名函数:

  1. public delegate int Foohandle(int a, int b);
  2. Foohandle foohandle = delegate (int a, int b) { return a + b; };//匿名方法方式
  3. Foohandle foohandle1= (a, b)=> a + b;//Lambda 表达式方式
  4. foohandle.Invoke(2,2);//输出4
  5. foohandle1.Invoke(2,2);//输出4

 

我们来看下msdn是怎么定义匿名函数的:

 

很明显,匿名函数只是个表达式,可以用来初始化委托的,而委托是个类,其实通过查看IL,后台都会实例化一个新的委托对象,并把该表达式作为函数赋给它

  • 委托与事件的关系?

 同样的我们来声明和使用事件:

  1. public class Foo
  2. {
  3. public delegate void Foohandel(int a, int b);
  4. public event Foohandel foohandle;
  5. public Foo()
  6. {
  7. foohandle = new Foohandel(add);
  8. foohandle(2,2);//在Foo里面可以直接调用事件
  9. Console.WriteLine($"{foohandle.Target},{foohandle.Method}");
  10. }
  11. public void excute(int a,int b)//公开给外部类调用事件的函数
  12. {
  13. foohandle?.Invoke(a,b);
  14. }
  15. private void add(int a, int b)
  16. {
  17. Console.WriteLine(a + b);
  18. }
  19. }
  20. class Program
  21. {
  22. static void Main(string[] args)
  23. {
  24. Foo foo = new Foo();
  25. //foo.foohandle = new Foo.Foohandel(multiply);编译不过,提示foo.foohandle只能出现再+=和-=左边
  26. foo.foohandle +=new Foo.Foohandel(multiply);
  27. foo.excute(2, 2);
  28. Console.Read();
  29. }
  30. static void multiply(int a,int b)
  31. {
  32. Console.WriteLine(a * b);
  33. }
  34. }

 

输出结果:

  1. 4
  2. EventSample.Foo,Void add(Int32, Int32)
  3. 4
  4. 4

 

     我们发现,在Foo类里面,事件foohandle就是相当于委托,但是在外部,我们再program的main函数访问它时候,我们发现foohandle只能做+=或者-=,也不能访问其函数成员Target和Menthod,而我们只能通过调用excute函数去调用,这时候我们可以知道,Event其实是基于委托的,在内部类相当于委托,在外部就只能有委托的多播功能,其余都不能访问,其实我们想到,属性是不是这样。。。有兴趣的朋友可以去了解事件的原理,也是很有趣

最后的最后,我们还要谈下委托的一个功能:

委托的参数逆变和返回值的协变

由于委托也支持泛型委托,因此我们可以看看微软定义好的

  1. public delegate void Action<in T>(T obj);//其中in表示逆变
  2. public delegate TResult Func<out TResult>();//其中out表示协变
  3.  
  4. class Program
  5. {
  6. static Action<object> action;
  7. static Func<string> func;
  8. static void Main(string[] args)
  9. {
  10. action = (object a) => { Console.WriteLine(a.ToString()); };
  11. Action<string> action1 = action;//参数逆变
  12. action("Hello!");
  13. func = () => { return "I am Func"; };
  14. Func<object> func1 = func;//返回值协变
  15. Console.WriteLine(func1());
  16. Console.ReadLine();
  17. }
  18. }

 

输出结果:

  1. Hello!
  2. I am Func

想要了解更深的朋友可以去了解泛型的协变和逆变,在这里就不深入探讨了

 

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

版权声明:本文为ryzen原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/ryzen/p/12009602.html