您可能已经对事件进行编程若干年了,但是迁移到 .NET Framework 仍然需要您重新检查事件的内部工作,因为 .NET Framework 中的事件位于委托的顶层。 对委托的了解越多,对事件进行编程时所具有的驾驭能力越强。 开始使用公共语言运行库 (CLR) 的某个事件驱动框架(例如 Windows? Forms 或 asp.net)时,理解事件在较低的级别如何工作至关重要。 本月我的目标是使您理解事件在较低的级别如何工作。
什么是事件?
事件是一种形式化的软件模式,在该模式中,通知源将对一个或多个处理程序方法进行回调。 因此,事件类似于接口和委托,因为它们提供了设计使用回调方法的应用程序的方法。 但是,事件极大地提高了工作效率,因为它们使用起来比接口或委托更轻易。 事件答应编译器和 Visual Studio? .NET IDE 在幕后为您做大量的工作。
涉及事件的设计基于事件源和一个或多个事件处理程序。 事件源可以是一个类也可以是一个对象。 事件处理程序是绑定到处理程序方法的委托对象。 图 1 显示了绑定到其处理程序方法的事件源的高级别视图。
图 1 事件源和处理程序
每个事件都是根据特定的委托类型定义的。 对于事件源定义的每个事件,有一个基于事件的基础委托类型的私有字段。 该字段用于跟踪多路广播委托对象。 事件源还提供答应您注册所需数量的事件处理程序的公用注册方法。
当您创建事件处理程序(委托对象)并在事件源中注册它时,事件源只是将新的事件处理程序追加到列表的结尾。 然后,事件源可以使用私有字段在多路广播委托上调用 Invoke,该多路广播委托将依次执行所有注册的事件处理程序。
事件的真正的妙处在于对其进行设置的大量工作都已经为您做好了。 正如您很快就会看到的,无论任何时候您定义事件时,Visual Basic? .NET 编译器都会通过自动添加私有委托字段和公用注册方法帮助您工作。 您还将看到 Visual Studio .NET 可以通过代码生成器提供更多的帮助,代码生成器可以自动发出适用于您的处理程序方法的主干定义。
对事件进行编程
由于 .NET 中的事件建立在委托的顶层,因此它们的基础的管道具体信息与较低版本的 Visual Basic 中所一直使用的截然不同。 但是,Visual Basic .NET 的语言设计者们在保持事件编程的语法与较低版本的 Visual Basic 一致方面做得很好。 在很多情况下,对事件进行编程涉及的语法与您习惯使用的熟悉的老语法相同。 例如,您将使用 Event、RaiseEvent 和 WithEvents 等要害字,而它们的行为方式与其在较低版本的 Visual Basic 中的行为方式几乎完全相同。
让我们通过创建一个基于事件的简单的回调设计开始。 首先,我需要通过使用 Event 要害字在类定义内定义一个事件。 必须根据特定的委托类型定义每个事件。 下面是定义自定义委托类型和用来定义事件的类的一个示例:
Delegate Sub LargeWithdrawHandler(ByVal Amount As Decimal)
Class BankAccount
Public Event LargeWithdraw As LargeWithdrawHandler
'*** other members omitted
End Class
在本示例中,LargeWithdraw 事件被定义为实例成员。 在本设计中,BankAccount 对象将充当事件源。 假如希望类而不是对象充当事件源,应该使用 Shared 要害字将事件定义为共享成员。
对事件进行编程时,知道编译器在幕后为您做了大量额外的工作这一点很重要。 例如,当您将我刚才向给您看过的 BankAccount 类的定义编译到程序集时,您认为编译器会做什么? 图 2 显示了在中间语言反汇编程序 ILDasm.exe 中检查生成的类定义时,该定义是什么样的。 该视图毫无保留地向您显示了 Visual Basic .NET 编译器在幕后做了多少工作来帮助您。
图 2 ILDasm 中的类定义
当您定义事件时,编译器在类定义内生成四个成员。 第一个成员是基于委托类型的私有字段。 该字段用于跟踪对委托对象的引用。 编译器通过采用事件本身的名称并添加后缀“Event”生成该私有字段的名称。 这意味着创建名为 LargeWithdraw 的事件将导致创建名为 LargeWithdrawEvent 的私有字段。
编译器还生成两个方法,帮助注册和注销将成为事件处理程序的委托对象。 这两个方法使用标准的命名规则进行命名。 用于注册事件处理程序的方法使用事件的名称,并带有前缀“add_”。 用于注销事件处理程序的方法使用事件的名称,并带有前缀“remove_”。 因此,为 LargeWithdraw 事件创建的两个方法名为 add_LargeWithdraw 和 remove_LargeWithdraw。
Visual Basic .NET 编译器通过调用 Delegate 类的 Combine 方法为将委托对象作为参数接受并将其添加到处理程序列表中的 add_LargeWithdraw 生成一个实现。 编译器通过在 Delegate 类中调用 Remove 方法为从列表中删除一个处理程序方法的 remove_LargeWithdraw 生成一个实现。
第四个也是最后一个添加到类定义中的成员是表示事件本身的成员。 在图 2 中,您应该能够找到名为 LargeWithdraw 的事件成员。 它是旁边带有一个倒三角的成员。 但是,您应该注重到,该事件并不象其它三个成员一样真的是一个物理成员。 相反,它是一个仅包含元数据的成员。
此仅包含元数据的事件成员很有价值,因为它可以向该类支持的编译器和其他开发工具通知 .NET Framework 中事件注册的标准模式。 该事件成员还包含注册方法和注销方法的名称。 这使得 Visual Basic .NET 和 C# 等托管语言的编译器可以在编译时查找注册方法的名称。
Visual Studio .NET 是查找此仅包含元数据的事件成员的开发工具的另一个很好的示例。 当 Visual Studio .NET 发现类定义包含事件时,它将自动生成处理程序方法的主干定义以及将它们作为事件处理程序进行注册的代码。
在开始讨论激发事件之前,我想提出一个有关用于定义事件的委托类型的限制。 用于定义事件的委托类型不能有返回值。 您必须使用 Sub 要害字而不是 Function 要害字定义委托类型,如下所示:
'*** can be used for events
Delegate Sub BaggageHandler()
Delegate Sub MailHandler(ItemID As Integer)
'*** cannot be used for events
Delegate Function QuoteOfTheDayHandler(Funny As Boolean) As String
对此限制有很充分的原因。 当涉及与若干处理程序方法绑定的多路广播委托时,处理返回值相当困难。 在多路广播委托上调用 Invoke 返回与调用列表中的最后一个处理程序方法相同的值。 但是,捕捉较早在列表中出现的处理程序方法的返回值并不那么简单。 不需要捕捉多个返回值只会使事件更轻易使用。
激发事件
现在,让我们修改 BankAccount 类使其在提款数量超出 $5000 阈值时能够激发一个事件。 激发 LargeWithdraw 事件的最简单的方法是在一个方法、属性或构造函数的实现中使用 RaiseEvent 要害字。 您可能会觉得该语法很熟悉,因为它类似于您在较低版本的 Visual Basic 中使用的语法。 下面是从 Withdraw 方法激发 LargeWithdraw 事件的一个示例:
Class BankAccount
Public Event LargeWithdraw As LargeWithdrawHandler
Sub Withdraw(ByVal Amount As Decimal)
'*** send notifications if required
If (Amount 5000) Then
RaiseEvent LargeWithdraw(Amount)
End If
'*** perform withdrawal
End Sub
End Class
虽然语法与较低版本的 Visual Basic 相同,但是激发事件时所发生的事情则与现在截然不同。 使用 RaiseEvent 要害字激发事件时,Visual Basic .NET 编译器生成执行每个事件处理程序所需的代码。 例如,当您编译以下代码时您认为会出现什么情况?
RaiseEvent LargeWithdraw(Amount)
Visual Basic .NET 编译器将此表达式扩展为在保留多路广播委托对象的私有字段上调用 Invoke 的代码。 换句话说,使用 RaiseEvent 要害字与在以下 snippet 中编写代码具有完全相同的效果:
If (Not LargeWithdrawEvent Is Nothing) Then
LargeWithdrawEvent.Invoke(Amount)
End If
注重,Visual Basic .NET 编译器生成的代码执行检查以确保 LargeWithdrawEvent 字段包含对某个对象的有效引用。 这是因为 LargeWithdrawEvent 字段的值在第一个处理程序方法注册之前一直为 Nothing。 因此,除非当前至少有一个处理程序方法已注册,否则生成的代码并不尝试调用 Invoke。
您应该能够对激发事件进行观察。 使用 RaiseEvent 要害字或者根据编译器自动生成的 LargeWithdrawEvent 私有字段直接进行编程通常并没有什么分别。 两种方法都生成相同的代码:
'*** this code
RaiseEvent LargeWithdraw(Amount)
'*** is the same as this code
If (Not LargeWithdrawEvent Is Nothing) Then
LargeWithdrawEvent.Invoke(Amount)
End If
在很多情况下,您可能喜欢使用 RaiseEvent 要害字语法,因为它需要的键入较少,生成的代码较简洁。 但是,在某些情况下,当您需要较多控制时,根据 LargeWithdrawEvent 私有字段进行明确编程可能会有意义。 让我们看一个这种情况的示例。
想象以下情况:BankAccount 对象有三个事件处理程