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#中委托是作为一个特殊的类型(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:
当你定义一个事件时,编译器为了实现事件的功能会自动加上两个方法来提供“订阅”和“取消订阅”的功能。
订阅是什么?“订阅就是调用定义事件时自动生成的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