C#基础知识梳理系列五:委托与事件(上)

摘 要

       委托与事件,这是一个老生常谈的话题,很多人在讲,很多人在用,但似乎它是一个永远也说不完道不尽的东西。那么,到底什么是委托?什么是事件?委托链又是怎么回事?为什么使用事件时常常用到+=/-=?委托又是如何支持协变和逆变的呢?你喜欢使用Action和Func吗?由于内容比较多,这一章将分上、下两部分慢慢为你讲解。

第一节 委托

       回调函数是Windows编程语言中一种常见而有用的编程实践,在C/C++中,它指的是函数调用的指针,通过这个指针可以方便地对函数进行调用,当然这个指针也是可以被传递给别的函数使用。在.NET Framework中,回调是通过委托来实现的,当然在这里,比起非托管的C/C++,.NET中的委托提供了更丰富的功能,比如同步和异常调用、委托链等等。

       委托其实是一种类型,是一种定义方法签名的类型,它支持以new的方式来实例化。委托是使用关键字delegate进行定义的,它其实是对方法的包装和聚集。既然它也是一种类型,所以能定义类的地方都可以定义委托,如下是一个委托的声明:

 
    
public delegate void ShowMessage(string msg);

       任何与委托签名匹配的方法都可以分配给委托,这就要求该方法的返回值类型与参数列表必须与委托的签名相匹配,方法可以是静态的,也可以是对象级的,通过委托可以对分配给委托的方法进行调用。我们来看一下编译器来干了什么事:

C#基础知识梳理系列五:委托与事件(上)_第1张图片

       通过上图我们可以看出,编译器让这个委托类型继承了System.MulticastDelegate类,System.MuticastDelegate类又继承了Delegate类,如下:

public abstract class MulticastDelegate : Delegate
{
    //...
}

       同时还生成了三个方法,其中BeginInvoke()和EndInvoke()两个方法是供异步调用,Invoke()方法是供同步调用。其实通过IL更能明确的看出委托最终经过编译器生成的是一个类:

C#基础知识梳理系列五:委托与事件(上)_第2张图片

       在Delegate类中有两个非常重要的字段:

       internal object _target; 当委托包装的是一个静态方法时,该字段为null;当委托包装的是一个对象方法时,该字段引用的是该对象。该字段可以通过属性Target获取。

       internal IntPtr _methodPtr; 保存着一个方法的IntPtr值,属性Method 获取一个标识了该回调方法的对象(MethodInfo)的引用,在类的内部,这个MethodInfo对象是通过方法GetMethodImpl()运算生成来的。

       我们分别定义一个实例级方法ShowString()和一个静态方法StaticShowString(),代码:

    void ShowString(String str)
    {
        Console.WriteLine("ShowString:" + str);
    }
    public class Code_05_01
    {
        public static void StaticShowString(string str)
        {
            Console.WriteLine("StaticShowString:" + str);
        }
    }

现在我们来看一下运行时的这两个属性,如下图:

C#基础知识梳理系列五:委托与事件(上)_第3张图片

C#基础知识梳理系列五:委托与事件(上)_第4张图片

上图中显示了Target指向的是对象,下图中由于绑定了一个静态方法,所以Target是null。

MulticastDelegate类有两个私有字段:

private IntPtr _invocationCount; 保存了委托链中方法个数。

private object _invocationList; 保存的即是委托链(方法集合)

MulticastDelegate类重写了Delegate的一个虚方法public virtual Delegate[] GetInvocationList(),来获取委托的调用列表。

对委托的调用也有两种方式,可以同步调用也可以异步调用,同步调用有两种方法:直接调用和使用委托对象的Invoke方法,如下:

void TestCall()
        {
            ShowMessage callShow = new ShowMessage(ShowString);
            callShow("abc");
        }
        void TestInvoke()
        {
            ShowMessage invokeShow = new ShowMessage(ShowString);
            invokeShow.Invoke("abc");
        }


 

这两种调用有什么区别呢?我们来看一下它们的IL。

TestCall.IL:

.method private hidebysig instance void  TestCall() cil managed
{
  // 代码大小       27 (0x1b)
  .maxstack  3
  .locals init ([0] class ConsoleApp.Example05.ShowMessage callShow)
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  ldftn      instance void ConsoleApp.Example05.Code_05::ShowString(string)
  IL_0008:  newobj     instance void ConsoleApp.Example05.ShowMessage::.ctor(object,
                                                                             native int)
  IL_000d:  stloc.0
  IL_000e:  ldloc.0
  IL_000f:  ldstr      "abc"
  IL_0014:  callvirt   instance void ConsoleApp.Example05.ShowMessage::Invoke(string)
  IL_0019:  nop
  IL_001a:  ret
} // end of method Code_05::TestCall

TestInvoke.IL:

.method private hidebysig instance void  TestInvoke() cil managed
{
  // 代码大小       27 (0x1b)
  .maxstack  3
  .locals init ([0] class ConsoleApp.Example05.ShowMessage invokeShow)
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  ldftn      instance void ConsoleApp.Example05.Code_05::ShowString(string)
  IL_0008:  newobj     instance void ConsoleApp.Example05.ShowMessage::.ctor(object,
                                                                             native int)
  IL_000d:  stloc.0
  IL_000e:  ldloc.0
  IL_000f:  ldstr      "abc"
  IL_0014:  callvirt   instance void ConsoleApp.Example05.ShowMessage::Invoke(string)
  IL_0019:  nop
  IL_001a:  ret
} // end of method Code_05::TestInvoke

       其实两者的内部调用实现基本一样,都是对委托对象的方法Invoke进行调用。

       委托链 也叫多路广播委托,是在委托内部由委托对象构成的一个委托对象集合,可以通过委托来调用委托链内的所有委托包装的方法。Delegate有两个静态方法,Combine()用于创建委托链和委托链添加新的委托,我们假设有一个委托委托A,来模拟向委托链追加委托的过程:

(1) 委托A对象在实例化的时候已经包装了一个方法,

(2) 当调用Delegate.Combine()方法向委托A追加新委托B对象时,在内部会重新创建一个委托对象C,并且用新追加的委托(方法)初始化_target和_methodPtr字段;

(3) 将委托C的_invocationList初始化为一个委托对象数组,并将委托A放到这个数组的第1项(索引为0)位置,然后将新的委托B对象放到数据的第2项位置(这里会根据委托的个数依次递增,新增加的那个委托对象总是在这个数组的最后位置),最后返回这个新创建的委托对象C。

       当再次向新委托C追加委托成员时,会重复(1)-(3)的步骤,每次最终都会返回一个新创建的委托,并且字段_target和_methodPtr总是根据新增加的委托对象来实例化,不过此时的这两个字段好像用处已经不大了,它只是保存了是最后进来的一个委托对象的部分数据。很显然,如果一个委托只包装了一个方法,并没有因追加新的委托而创建委托链,那么在这种情况下,这两个字段_target和_methodPtr是非常有意义的。

       与方法Combine()对应的有一个方法public static Delegate Remove(Delegate source, Delegate value);很显然它是从委托链中移除一个委托对象。为了方便书写,C#为委托类型的实例重载了两个操作符+=和-=分别对应于方法Combine和方法Remove。通过下图我们可以看一下追加委托的过程。继续对上面的代码进行改造,增加一个对象级方法:

void ShowString2(String str)
        {
            Console.WriteLine("ShowString2:" + str);
        }

然后实例化一个委托ShowMessage show3 = new ShowMessage(ShowString);,如下图:

C#基础知识梳理系列五:委托与事件(上)_第5张图片

       可以看到此时show3的Method指向的方法是ShowString(System.String),并且字段_invocationCount的值为0,_invocationList是null。

       接下来我们向show3追加一个委托对象(事实上是向委托链追加),show3 += new ShowMessage(ShowString2);,也可以使用简写:show3 += ShowString2;这样不用手写代码来创建委托对象,但在编译的过程中,编译器还是会识别出这是一个创建委托对象的过程并向IL中写入创建委托对象的代码。如下图:

C#基础知识梳理系列五:委托与事件(上)_第6张图片

此时,我们已经看到Method指向的是新的方法ShowString2(System.String),_invocationCount的值为2,_invocationList已经是一个拥有两个元素的集合。

对委托对象有委托链且不为空的时候,又是如何调用委托链内的各个回调函数的呢?通过上面对Invoke的讨论,我们知道当调用一个委托对象的回调函数时,在内部CLR实际上是调用了Invoke方法,而在调用invoke方法时,该委托会发现字段_invocationList不为null,接着就会遍历该数组中的所有委托对象依次对委托方法进行调用。

协变性与逆变性

委托的协变性 是指委托方法能返回从对应委托的返回类型派生的一个类型。

委托的逆变性 是指方法获取的参数类型可以是委托的参数类型的基类。

这里的描述的有点绕口,我们来年如下代码:

public class Code_05_02
    {
        public string Name { get; set; }
    }
    public class Code_05_03 : Code_05_02
    {
        public int Age { get; set; }
    }
//定义一个委托MyDel
public delegate Code_05_02 MyDel(Code_05_03 para);
//定义一个与委托MyDel 相匹配的方法
        private Code_05_03 GetData(Code_05_02 para)
        {
            return new Code_05_03();
        }
//以下的实例化及调用是可行的。
            MyDel del = new MyDel(GetData);
            del(new Code_05_03());

Code_05_03类继承于Code_05_02类,委托MyDel的返回类型是Code_05_02,方法GetData的返回类型是Code_05_03,这体现的协变性;方法GetData的参数类型是Code_05_02,委托的参数类型是Code_05_03,这体现了逆变性。

需要说明一点的是:协变性和逆变性不能用于值类型(包括void)。

在我们开发的过程中,可能经常要使用委托,但委托的定义都大同小异,很幸运的是.NET Framework为我们预定义了很多的常用泛型委托。

无返回值的Action系列:

public delegate void Action<in T>(T obj)
public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2)
//共有16个,另外还有一个无参的非泛型委托:
public delegate void Action()

使用非常简单,代码示例:

void TestAction()
        {
            Action<string> act = new Action<string>(ShowString);
            act("Action");
        }

有返回值的Func系列:

public delegate TResult Func<in T, out TResult>(T arg)
public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2)
//共有16个,另外还有一个无参的泛型委托:
public delegate TResult Func<out TResult>()

一般的时候,我们使用这些委托已经足够了。详细内容可以查询MSDN: Action系列 和 Func系列 。

C#的lambda表达式为委托的简化使用显示出了很不错的编程体验。可以参考MSDN的相关章节:Lambda 表达式(C# 编程指南) 。

这一章的上半部分,我们主要讲解了与委托相关的内容,后一篇的下半部分将主要讲解什么是事件及委托如何与事件共事

 

 

你可能感兴趣的:(C#基础知识梳理系列,.NET(c#))