在讨论事件订阅之前,我们先来看看委托的另一个特性,即调用列表(invocation list)。对于某个特定的委托而言,我们既可以将其它的委托加入其调用列表中,也可以从其调用列表中将其它的委托移除。那么当程序使用这个委托的时候,就会循环遍历并执行其调用列表中的所有委托。
下面请看一个例子,在这个例子中,我们定义了一个委托Callback,并将指代两个函数Func1和Func2的委托依次加入其调用列表中。在调用委托的时候,我们发现,Func1和Func2被依次调用。
view plaincopy to clipboardprint?
class Program
{
delegate void Callback();
static void Func1()
{
Console.WriteLine("Hello");
}
static void Func2()
{
Thread.Sleep(3000);
Console.WriteLine("World");
}
static void Main(string[] args)
{
Callback cb = null;
cb = (Callback)Callback.Combine(cb, new Callback(Func1));
cb = (Callback)Callback.Combine(cb, new Callback(Func2));
cb();
}
}
上面的代码会依次输出“Hello”和“World”。细心的读者已经发现,在输出“Hello”之后程序停顿了3秒钟,也就是说,委托的调用列表是顺序同步执行的,其执行顺序与调用列表中委托的加入顺序相同。
现在我们的讨论回到事件上来。我们看看,在C#中,事件的本质究竟是什么。在使用reflector对我们开发的EventDemo例子进行反编译后,我们可以看到,在Started和Stopped事件节点下多出了两个方法:add_xxx和remove_xxx(xxx是事件的名称)。在reflector右边的窗口中,还能看到这两个方法的源代码。
view plaincopy to clipboardprint?
[MethodImpl(MethodImplOptions.Synchronized)]
public void add_Started(ServerEventHandler value)
{
this.Started = (ServerEventHandler) Delegate.Combine(this.Started, value);
}
view plaincopy to clipboardprint?
[MethodImpl(MethodImplOptions.Synchronized)]
public void remove_Started(ServerEventHandler value)
{
this.Started = (ServerEventHandler) Delegate.Remove(this.Started, value);
}
原来,事件在C#中会被编译器翻译成两个方法(add_xxx和remove_xxx),在这两个方法中,会使用Delegate的Combine和Remove方法来将给定的事件处理委托添加到调用列表中。那么,当多个对象对事件进行订阅后,一旦事件触发,那么调用列表中的委托也将依次执行。注意每个方法上面的MethodImpl特性,该特性是用来保证线程安全的,防止多个线程在同一时刻访问调用列表而出现冲突(该话题今后讨论)。
为了使用的方便,C#使用“+=”和“-=”运算符实现事件订阅,事实上也就是在调用add_xxx和remove_xxx方法。这个过程由编译器自动解析并处理。请看下面的IL代码:
view plaincopy to clipboardprint?
IL_0014: callvirt instance void [EventDemo.Lib]EventDemo.Lib.Server::add_Started(class [EventDemo.Lib]EventDemo.Lib.Server/ServerEventHandler)
这行代码就是EventDemo示例中,Main函数订阅server实例Started事件的IL代码。明显看不到“+=”运算,取而代之的是对add_Started方法的调用。
本文简单的剖析了C#中的事件模型。事实上对于委托是如何处理调用列表这一问题,我们也可以通过reflector或者ildasm.exe来继续深究,其本身应该就是维护一个委托的数组。在下文中,将会对事件的最后一个话题:add和remove关键字进行讨论。