【C#进阶系列】17 委托


委托主要是为了实 现回调函数机制,可以理解为函数指针(唯一不同的在于多了委托链这个概念)。

然而用的时候可以这么理解,但是委托的内部机制是比较复杂的。

一个委托的故事

delegate void razor(String userName);

一个简单的委托被定义了。

实际上在编译后这段代码就和下面的代码很像了:

  class Razor : System.MulticastDelegate {
        //构造函数
        public Razor(Object @object,IntPtr method);
        public virtual void Invoke(Int32 value);
        //实现对回调方法的异步回调
        public virtual IAsyncResult BeginInvoke(Int32 value, AsyncCallback callback, Object @object);
        public virtual void EndInvoke(IAsyncResult result);
    }

也就是说委托实际上是一个派生自MulticastDelegate的类,其中有四个方法。

而由于继承MulticastDelegate类,所以还继承了MulticastDelegate类的字段、方法和属性。

有三个字段最重要,分别是指向委托传进的实例方法的操作对象_target(如果是静态方法就是NULL),CLR用来标识回调方法的_methodPtr(这里不是指针,而是一个内部的参数值,用以表示某方法),构造委托链时委托数组的引用_invokationList(在委托通过Delegate.Combine或者+=绑定了多个回调函数后才有值,否则为null).

而在使用委托的时候会:

先去看此委托的_invokationList是否为null,为null就说明没有建立委托链,那么实际上就用了委托Razor(实际上是一个类的实例),调用Invoke方法,此时会传给Invoke方法一个参数值,它就是_methodPtr存放的内部的参数值,用于标识传进来的实例方法的参数值。

如果_invokationList不为null说明已经建立了委托链,那么久分别去调用委托数组里面各个委托的Invoke方法,执行的方法自然是各个委托里面_methodPtr指定的方法。

而至于移除委托(用Delegate.Remove或者-=)。

执行委托链中的函数是按照绑定的顺序开始执行,但是如果有返回,那么返回的一定是最后一个绑定的函数的返回结果。

然而这种执行委托链的方式有一些问题,比如某个委托链中的委托抛了异常或者因为调用数据库查询的阻塞时间太长,导致后面的委托函数都无法执行。

所以MulticastDelegate类提供了一个实例方法GetInvokationList(),他会返回_invokationList指向的Delegate数组。

泛型委托

泛型委托是为了解决相似的委托过多的问题,而且.NET提供的泛型委托也很简单,17个参数不同的Action委托和17个参数不同的Func委托.

其中Action委托都是返回值为void,而Func委托的返回值类型为自己指定的类型的TResult。

然而如果需要使用ref和out关键字以传引用的方式传递参数,那么就不得不定义自己的委托。因为泛型委托的协变和逆变的时候会用到in和out标识,为了搞混就不能这么用。(此处可参考泛型那一章)

C#关于委托的语法糖

一个正常的委托是下面这样的

    static void Main(string[] args)
        {
            Razor razor=null;
            razor += new Razor(Blower);
            razor("Troy123");
            Console.Read();
        }
        static void Blower(String userName) {
            Console.WriteLine(userName + ":实际上这是一个吹风机");
        }

然而

razor += new Razor(Blower);

这样的语法看起来很奇怪(一开始奇怪,其实还好啦),所以C#提供个一些简化的语法:

  • 不需要构造委托对象
    • 一些函数会像下面那样可以直接传入回调方法,而不是像之前一样需要new Razor这样去调用构造委托对象。表面上是如此,实际上只是在内部做了处理,还是会去构造委托对象然后调用。

              void 回调函数(object obj) {
                  Console.WriteLine(obj);
              }    
              ThreadPool.QueueUserWorkItem(回调方法, 5);
  • 不需要定义回调方法(lambda表达式)
    • 之前的代码可以写成这样
      ThreadPool.QueueUserWorkItem(l=>Console.WriteLine(o),5);
    • lambda表达式这种方法看起来很怪异,然而用习惯了非常好用。这是通过创建匿名函数去取代回调函数。(这种玩法虽然好,但是如果一个回调函数被多个地方调用,那么还是用之前的方法好一点)
    • 匿名函数一般被标记为static和private。前者是因为不需要创建额外的this参数所以效率更高,后者是为了安全性。(除非匿名函数用了实例成员)
      • 以下是一些具体例子:
        delegate void Bar(out Int32 z);
        
        
          Func<string> f = () => "Troy";
           Funcstring> f1 = (Int32 l) => l.ToString();
           Funcstring> f2 = (l) => l.ToString();//推断类型
           Funcstring> f3 = (l, m) => { Console.WriteLine(m); return l.ToString(); };//多条语句加大括号
           Bar b = (out Int32 n) => n = 5;//对于引用类型,必须显示指定
  • 局部变量不需要手动包装到类中即可传给回调方法
    • 看起来很绕,实际上很简单。就是lambda表达式可以直接使用局部变量和参数,而不需要你去传参。

委托和反射

System.Delegate.MethodInfo提供了一个CreateDelegate方法,顾名思义,就是去动态创建一个委托。

然后可以用Delegate.DynamicInvoke方法去动态调用它。

关于这个具体的也没有想到太多的地方会用到,毕竟见的世面少了。