C#--委托(Delegate)和事件(Event)

委托在本质上仍然是一个类,我们用delegate关键字声明的所有委托都继承自System.MulticastDelegate。后者又是继承自System.Delegate类,System.Delegate类则继承自System.Object。委托是一种类型安全的函数回调机制, 它不仅能够调用实例方法,也能调用静态方法,并且具备按顺序执行多个方法的能力。

1. 委托也好,事件也好最初的起源是C/C++中的函数指针,什么情况下我们需要函数指针。函数指针最常用的方式就是回调(callback)——在函数内回调主函数里的函数。

#include <iostream.h>
#include <list>
using namespace std;

void max(int a, int b)
{
    cout<<"now call max("<<a<<","<<b<<")..."<<endl;
    int t = a>b?a:b;
    cout<<t<<endl;
}
void min(int a, int b)
{
    cout<<"now call min("<<a<<","<<b<<")..."<<endl;
    int t = a<b?a:b;
    cout<<t<<endl;
}
typedef void (*myFun)(int a, int b); //定义一个函数指针用来引用max,min


//回调函数
void callback(myFun fun, int a, int b)
{
    fun(a,b);
}
void main()
{
    int i = 10;
    int j = 55;
    callback(max,i,j); 

    callback(min,i,j);
}
Output:
now call max(10,55)...
55
now call min(10,55)...
10
Press any key to continue

通过转入函数指针,可以很方便的回调(callback)另外一些函数,而且可以实现参观化具体需要回调用的函数。

2 .net里一向是"忌讳"提及"指针"的,"指针"很多程度上意味着不安全。C#.net里便提出了一个新的术语:委托(delegate)来实现类似函数指针的功能。我们来看看在C#中怎么样实现上面的例子。

using System;
namespace Class1
{ 
    class ExcelProgram
    {
        static void max(int a, int b)
        {
            Console.WriteLine("now call max({0},{1})",a,b);
            int t = a>b?a:b;
            Console.WriteLine(t);
        }
        static void min(int a, int b)
        {
            Console.WriteLine("now call min({0},{1})",a,b);
            int t = a<b?a:b;
            Console.WriteLine(t);
        }
        delegate void myFun(int a, int b); //定义一个委托用来引用max,min

        //回调函数
        static void callback(myFun fun, int a, int b)
        {
            fun(a,b);
        }
        
        static void Main(string[] args)
        {
            int i = 10;
            int j = 55;
            callback(new myFun(max),i,j); 
            callback(new myFun(min),i,j);
            Console.ReadLine();
        }
    }
}

3. 委托除了可以引用一个函数外,能力上还有了一些加强,其中有一点不得不提的是:多点委托(Multicast delegate).简单地讲就是可以通过一个申明一个委托,来调用多个函数。

static void Main(string[] args)
  {
   int i = 10;
   int j = 55;
 
   myFun mulCast = new myFun(max);
   mulCast += new myFun(min);
 
   callback(mulCast,i,j);   

   Console.ReadLine();
  }
输出如下:
now call max(10,55)...
55
now call min(10,55)...
10
Press any key to continue

实质是在C#中,delegate关键字指定的委托自动从System.MulticastDelegate派生,而System.MulticastDelegate是一个委托列表,在callback中只需调用mulCast的引用便可以以同样的参数调用该链表中的所有函数。

下图展示了刚才那段C#代码的IL(用ILDasm反汇编即可):

C#--委托(Delegate)和事件(Event)

在C#中委托是作为一个特殊的类型(Type,Object)来对待的,委托对象也有自己的成员:BeginInvoke,EndInvoke,Invoke。这几个成员是你定义一个委托时编译器帮你自动自成的,而且他们都是virtual函数,具体函数由运行时库来实现。

一个委托对象是怎么样关联到函数的呢,我们双击Main函数,可以看到以下IL,虽然IL语法复杂但仍不影响我们了解它是怎么样将一个委托关联到一个(或多个)函数的引用的。

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  .custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 ) 
  // 代码大小       58 (0x3a)
  .maxstack  4
  .locals ([0] int32 i,
           [1] int32 j,
           [2] class Class1.ExcelProgram/myFun mulCast)
  IL_0000:  ldc.i4.s   10
  IL_0002:  stloc.0
  IL_0003:  ldc.i4.s   55
  IL_0005:  stloc.1
  IL_0006:  ldnull
  IL_0007:  ldftn      void Class1.ExcelProgram::max(int32,
                                                     int32)
  IL_000d:  newobj     instance void Class1.ExcelProgram/myFun::.ctor(object,
                                                                      native int)
  IL_0012:  stloc.2
  IL_0013:  ldloc.2
  IL_0014:  ldnull
  IL_0015:  ldftn      void Class1.ExcelProgram::min(int32,
                                                     int32)
  IL_001b:  newobj     instance void Class1.ExcelProgram/myFun::.ctor(object,
                                                                      native int)
  IL_0020:  call       class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate,
                                                                                          class [mscorlib]System.Delegate)
  IL_0025:  castclass  Class1.ExcelProgram/myFun
  IL_002a:  stloc.2
  IL_002b:  ldloc.2
  IL_002c:  ldloc.0
  IL_002d:  ldloc.1
  IL_002e:  call       void Class1.ExcelProgram::callback(class Class1.ExcelProgram/myFun,
                                                          int32,
                                                          int32)
  IL_0033:  call       string [mscorlib]System.Console::ReadLine()
  IL_0038:  pop
  IL_0039:  ret
} // end of method ExcelProgram::Main

从上面的IL可以看出对于语句:

  myFun mulCast = new myFun(max);

是通过以max作为参数构建一个委托对象mulCast。但对于语句:

  mulCast += new myFun(min);

等价于:

  mulCast = (myFun) Delegate.Combine(mulCast, new myFun(min));

通过调用Delegate.Combine的静态方法将mulCast和min函数进行关联,Delegate.Combine方法将min函数的引用加至委托对象mulCast的委托链中。Delegate.Remove方法用来从委托链中移除一个委托,通过Invoke方法执行委托链。

事件是一种特殊的委托,委托链是一个委托的集合,它允许我们调用这个集合中的委托所代表的所有方法。

4. 事件/消息机制是Windows的核心,其实提供事件功能的却是函数指针,接下来我们再看看C#事件(Event),在C#中事件是一类特殊的委托。

事件的内部实现:

  1)一个委托类型的字段,用来保存事件发生时通知哪些对象。即通知所有订阅该事件的对象,别忘记C#中委托是支持多播的。

  2)两个方法,以委托类型为参数。作用是将订阅该事件的对象方法加至上面的委托类型字段中,以便事件发生后可以通过调用该方法来通知对象事件已发生。

using System;

namespace ConsoleApp2
{
    class Class21
    {
        static void Main(string[] args)
        {
            // 订阅事件
            ClassA.MethodHandle += Method;
            ClassA.MethodHandle += MethodA;
            // 触发事件
            ClassA.GetEvent();

            Console.Read();
        }

        public static void Method()
        {
            Console.WriteLine("调用了Method");
        }

        public static void MethodA()
        {
            Console.WriteLine("调用了MethodA");
        }
    }

    public class ClassA
    {
        public delegate void MethodCall();
        public static event MethodCall MethodHandle;

        public static void GetEvent()
        {
            // 通知所有已订阅事件的对象
            if(MethodHandle != null)
            {
                MethodHandle();
            }
        }
    }
}

反汇编ClassA:

C#--委托(Delegate)和事件(Event)

当你定义一个事件时,编译器为了实现事件的功能会自动加上两个方法来提供“订阅”和“取消订阅”的功能。

订阅是什么?“订阅就是调用定义事件时自动生成的add_MethodHandle.”

但C#并不能直接调用该方法,只能通过 "+=" 来实现。

让我们看看IL代码:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // 代码大小       50 (0x32)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldnull
  IL_0002:  ldftn      void ConsoleApp2.Class21::Method() // 先将Method方法压栈
  IL_0008:  newobj     instance void ConsoleApp2.ClassA/MethodCall::.ctor(object,
                        native int) // new一个委托对象
  IL_000d:  call       void ConsoleApp2.ClassA::add_MethodHandle(class 
ConsoleApp2.ClassA/MethodCall) // 通过调用add_MethodHandle把上面生成的委托加到事件的委托列表中
  IL_0012:  nop
  IL_0013:  ldnull
  IL_0014:  ldftn      void ConsoleApp2.Class21::MethodA()
  IL_001a:  newobj     instance void ConsoleApp2.ClassA/MethodCall::.ctor(object,
                                                                          native int)
  IL_001f:  call       void ConsoleApp2.ClassA::add_MethodHandle(class ConsoleApp2.ClassA/MethodCall)
  IL_0024:  nop
  IL_0025:  call       void ConsoleApp2.ClassA::GetEvent()
  IL_002a:  nop
  IL_002b:  call       int32 [mscorlib]System.Console::Read()
  IL_0030:  pop
  IL_0031:  ret
} // end of method Class21::Main