你也许使用事件编程已经多年了,但是迁移到.NET框架组件时仍需要重新检查它们的内部工作,因为在.NET框架组件中事件位于委托(delegate)的顶层。你对委托了解得越深,使用事件编程时就越能利用其强大的功能。在使用通用语言运行(common language runtime,CLR)的事件驱动框架(例如Windows Forms或ASP.NET)工作时,了解事件在低层是怎样工作的很重要。本文的目标就是让你了解事件在低层是怎样工作的。
事件到底是什么 事件仅仅是一种软件模式(pattern),在事件中通知源对一个或多个处理方法进行回调(callback)。因此事件与接口(interface)和委托(delegate)相似,因为它们都提供了一条途径来设计使用回调方法的应用程序。但是事件生产率更高,因为它比接口和委托更易使用。事件让编译器和Visual Studio .NET集成开发环境在后台为你做了很多工作。
包含事件的设计是基于一个事件源和一个或多个处理程序的。事件源可以是类或对象,事件处理程序是绑定到某个处理方法的委托对象。图1在较高层次显示了数据源与处理方法的联系。
图1.事件源和处理程序
每个事件都根据特定的委托类型定义。对于每个事件源定义的事件,都有一个基于事件下面的委托类型的专用字段,该字段用于跟踪多点传送的委托对象。事件源也提供了一个公共的注册方法,让你可以注册希望的事件处理程序。
当你建立一个事件处理程序(一个委托对象)并把它与事件源一起注册时,事件源简单地把新的事件处理程序添加到列表的结尾。接着事件源能使用专用字段调用多点传送委托对象的Invoke方法,该方法将执行所有已注册的事件处理程序。
事件真正好的地方是大多数设置工作已经被开发环境完成。你将看到,Visual Basic .NET编译器帮助你在定义事件时自动的添加一个私有字段和一个公共注册方法。你也会看到Visual Studio .NET通过自动生成处理方法的框架定义的代码生成器为你提供了更多帮助。
使用事件编程 由于在.NET中的事件建立在委托的顶层,所有它们下面的通道细节与早期版本的Visual Basic工作方式有很大的不同。但是Visual Basic .NET语言的设计者为了保持与早期Visual Basic版本语言的一致性做了大量的工作。在很多情况中,事件编程使用与原来相近的语法。例如,你将使用Event、 RaiseEvent和WithEvents等关键字,它们的行为与早期版本的相同。
我们建立一个简单的基于事件的回调设计。首先我需要使用Event关键字在类的定义中定义一个事件。事件必须根据特定的委托类型来定义。下面是一个委托类型定义和使用它定义事件的类:
Delegate Sub LargeWithdrawHandler(ByVal Amount As Decimal)
Class BankAccount Public Event LargeWithdraw As LargeWithdrawHandler '省略了其它成员 End Class |
在上面的例子中,LargeWithdraw事件被定义为一个实例成员。该设计中BankAccount对象将作为事件源。如果你希望用类代替对象作为事件源,你可以使用Shared关键字把事件定义为共享成员。
当使用事件编程时,肯定编译器在后台做了大量的工作也很重要。例如,当编译BankAccount类的定义时编译器做了什么?图2显示了使用ILDasm.exe(中间语言反编译器)查看类的定义结果。
图2. ILDasm中的类定义
当你定义事件时,编译器在类的定义中产生四个成员。第一个成员是基于委托类型的私有字段,它用于跟踪一个委托对象的引用。编译器产生的该私有字段的名字是事件的名字加上"Event"标识。这意味着建立LargeWithdraw事件的结果是建立了名为LargeWithdrawEvent的私有字段。
编译器也产生了两个方法帮助注册和取消注册作为事件处理程序服务的委托对象。这两个方法的命名使用了标准的命名转换。注册事件处理程序的方法的名字加了"add_"前缀,取消注册的方法前面加了"remove_"前缀。因此为LargeWithdraw事件建立的这两个方法名称为add_LargeWithdraw 和remove_LargeWithdraw。
Visual Basic .NET编译器为add_LargeWithdraw产生了实现代码,它接收以一个委托对象作为参数并通过调用委托类的Combine方法将它添加到处理程序列表。编译器也产生remove_LargeWithdraw的实现代码,它通过调用委托类的Remove方法从列表中删除一个处理方法。
添加到类定义中的第四个成员表现了事件本身。你能在图2中定位名为LargeWithdraw的事件成员。它有一个向下的三角形。但是你必须注意这个事件成员不是一个真的与其它三个相似的物理事件,它是一个元数据成员。
该元数据事件成员是有价值的,因为它通知编译器和其它开发工具该类支持.NET框架中的事件注册标准模式。该事件成员也包含注册和反注册方法的名字。这使Visual Basic .NET和C#等可管理语言的编译器能在编译时就发现注册方法的名称。
当Visual Basic .NET发现类定义中包含事件时,它自动在产生事件处理程序的注册代码时生成该处理方法的框架定义。
在讨论引发事件前,我将讲解建立用于定义事件的委托类型所涉及的限制。定义事件的委托类型不能有返回值,你必须使用Sub关键字而不能使用Function关键字:
'能被事件使用 Delegate Sub BaggageHandler() Delegate Sub MailHandler(ItemID As Integer)
'不能被事件使用 Delegate Function QuoteOfTheDayHandler(Funny As Boolean) As String |
该限制有一个很好的原因。在绑定到多个处理方法的多点传送委托中使用返回值非常困难。多点传送委托的Invoke调用返回的值是调用列表中最后一个处理方法的值。可是捕获列表中前面的处理方法的返回值就不直接了,消除捕获多个返回值的需求使事件更容易使用。