台湾“大选”马英九得票破700万自行宣布当选!
当前位置:金诺VB园文章教程网络文章 → 使用多路广播委托实现回调

使用多路广播委托实现回调

减小字体 增大字体 作者:佚名  来源:不详  发布时间:2008-1-31 16:51:21

使用多路广播委托实现回调

发布日期: 1/11/2005 | 更新日期: 1/11/2005

Ted Pattison

*

我在 2002 年 12 月的专栏中介绍了与委托相关的基本概念和编程技术,本月将继续就委托展开讨论。我将假设您已阅读了上期专栏,并且熟悉编程委托的基本内容。例如,您应知道如何定义委托类型,如何创建绑定到处理程序方法的委托对象,以及如何通过调用委托对象的 Invoke 方法执行处理程序的方法。

本月,我将创建一个基于委托的设计,该设计涉及到回调通知。我将演示委托如何使得以松散耦合方式创建此类设计策划功能为可能。之后,我将阐释委托如何通过名为多路广播的功能来支持将通知绑定到多个处理程序方法。

使用委托实现回调

假设您正在设计一个带有名为 BankAccount 的类(该类包含一个名为 Withdraw 的方法)的应用程序。假设您希望,只要BankAccount 对象所提取的金额大于 5000 美元时,此应用程序的另一部分就能做出响应。那么,您所设置的类的起点可能类似于如下所示:

Class BankAccount
Sub Withdraw(ByVal Amount As Decimal)
If (Amount > 5000) Then
'*** send notification to interested parties
End If
'*** perform withdrawal
End Sub
End Class

在本示例中,BankAccount 对象将担当通知源。这表示 BankAccount对象必须为侦听器提供一种在收到通知时表达其兴趣的方法。换句话说,BankAccount对象必须允许侦听器注册回调的处理程序方法。让我们从定义一个名为 LargeWithdrawHandler 的新委托类型开始:

Delegate Sub LargeWithdrawHandler(ByVal Amount As Decimal)

现在,需要修改担当通知源的 BankAccount类。通过添加两个成员可以完成此项操作。首先,我将添加一个名为“handler”的私有字段,该字段以委托类型LargeWithdrawHandler 的形式定义。其次,我将添加一个名为 RegisterHandler的方法,它允许其他代码注册委托对象,以接收回调通知:

Class BankAccount
Private handler As LargeWithdrawHandler
Sub RegisterHandler(ByVal handler As LargeWithdrawHandler)
Me.handler = handler
End Sub
'*** other class members omitted
End Class

如您所见,该 RegisterHandler 方法接受 LargeWithdrawHandler类型的单一参数。RegisterHandler 的实现将此值分配给名为 handler 的字段,以便 BankAccount能够跟踪委托对象并执行其处理程序方法。

使用委托对象调用 RegisterHandler 方法后,BankAccount类中的所有方法都可以通过调用注册委托对象上的 Invoke 来执行该程序处理方法。下面显示了如何从 BankAccount 类的Withdraw 方法中执行 Invoke 方法:

Sub Withdraw(ByVal Amount As Decimal)
'*** send notifications if required
If (Amount > 5000) AndAlso (Not handler Is Nothing) Then
handler.Invoke(Amount)
End If
'*** perform withdrawal
End Sub

Withdraw 方法执行一次检查操作,以确保处理程序字段包含有效的引用,而不是 Nothing 值。请记住,在调用RegisterHandler 之前,它将具有一个 Nothing 值。因此,必须防止您的代码尝试对未实例化的引用执行 Invoke 方法。

只要提取金额大于 5000 美元,就会写入 BankAccount 类,以发送基于委托的通知。现在我们来创建一个可以被接通以对这些通知做出响应的处理程序方法。请看以下代码所显示的类:

Class AccountHandlers
Shared Sub GetApproval(ByVal Amount As Decimal)
'*** block until manager approves withdrawal amount
End Sub
Shared Sub LogWithdrawToDB(ByVal Amount As Decimal)
'*** write withdrawal info to a database
End Sub
Shared Sub LogWithdrawToFile(ByVal Amount As Decimal)
'*** write withdrawal info to a log file
End Sub
End Class

利用适当的调用签名,已经写入了 GetApproval、LogWithdrawToDB 以及 LogWithdrawToFile 方法,因此,它们可作为由 BankAccount 对象所发出通知的处理程序方法。

现在,我们来编写一个简单的程序,将各部分连接起来。首先,该应用程序必须创建一个 BankAccount对象。其次,该应用程序必须创建一个绑定到目标处理程序方法(如 GetApproval)的委托对象。挂钩各个部分的最后一个步骤是调用RegisterHandler 方法,将引用传递给委托对象。

Module MyApp
Sub Main()
'*** create bank account object
Dim acc1 As New BankAccount()
'*** create delegate object and register callback method
acc1.RegisterHandler(AddressOf AccountHandlers.GetApproval)
'*** do something that triggers callback
acc1.Withdraw(5001)
End Sub
End Module

本示例没有使用 New 运算符来创建委托对象。相反,它使用了更为方便的简洁语法。由于 RegisterHandler方法需要一个 LargeWithdrawHandler 类型的参数,因此,可以只需使用 AddressOf运算符(其后是方法名)即可,如代码所示。

至此,就有了一个可通过委托执行回调通知的简单应用程序。当 Main 方法调用BankAccount 对象中的 Withdraw 方法并传递一个值为 5001 的参数时,Withdraw方法的实现使用由处理程序字段使用的委托对象来执行 GetApproval 方法。

请注意,该应用程序提供了一个松散耦合设计的良好范例。BankAccount 类并不了解或不关心使用何种类型的处理程序方法。使用处理程序方法 LogWithdrawToDB 或LogWithdrawToFileIt 来替代 GetApproval处理程序方法将非常简单。此外,在不同的类中创建另外一个方法并使用此方法作为替代的回调方法也会非常简单。从此示例可以得知,基于委托的设计可通过与基于接口的设计相同的方式提供多态性。

此外,还有一个需要解决的更为重要的设计问题。当前 BankAccount的实现只能提供单个处理程序方法的回调。如果可以对 BankAccount类进行修改,使其能够同时提供多个处理程序方法的回调,就会使设计更加完善。幸运的是,通过一种名为多路广播的功能,委托为处理多个处理程序方法提供内置支持。

多路广播

每个委托类型都对处理多个处理程序具有内置支持。委托类型通过从 System命名空间中定义的MulticastDelegate类进行继承,获得了这种支持。多路广播的好处是,可以在一个列表中组合若干处理程序,从而将它们全部绑定到单个委托对象。当在委托对象中调用Invoke 时,MulticastDelegate 类提供执行列表中每个处理程序方法的代码。

通过一个示例,可以帮助您明确了解具体的工作方式。假设您愿意将两个不同的处理程序方法绑定到单个的委托对象。通过调用 System.Delegate 类的一个共享方法 Combine可以实现此目的。如果调用 Combine方法并传递两个委托对象,则该方法将返回一个新的委托对象,该对象是其他两个委托对象的多路广播。以下是一个示例,其中采用了两个不同委托对象的处理程序方法,并将这两个委托对象组合为一个多路广播委托对象:

'*** create two individual delegates
Dim handler1, handler2 As LargeWithdrawHandler
handler1 = AddressOf AccountHandlers.GetApproval
handler2 = AddressOf AccountHandlers.LogWithdrawToDB
'*** combine delegates into multicast delegate
Dim result As [Delegate] = [Delegate].Combine(handler1, handler2)
'*** convert reference to LargeWithdrawHandler type
Dim handlers As LargeWithdrawHandler
handlers = CType(result, LargeWithdrawHandler)
'*** execute handler methods in multicast list
handlers.Invoke(5001)

请注意将 [Delegate] 类名括起来的方括号。这两个方括号是必须的,因为 Delegate 也是一个关键字。这两个括号用作转义符,通知 Visual Basic®.NET 编译器您将使用 Delegate 作为类名。

对Combine 的调用会返回一个指向新创建的多路广播委托对象的引用。请注意,Combine 方法具有 Delegate的返回泛型,需要转换为更特定的委托类型 LargeWithdrawHandler。只有在调用 Invoke 方法时,才需要进行这种转换。

让我们花点时间来探讨一下如何实现多路广播委托。多路广播委托只是一个链接的委托对象列表。每个委托对象的私有实现包含一个用于保留引用的字段,该引用指向列表中前一个委托对象。您也许会认为,如果委托保留一个指向列表中下一个委托(而不是前一个)的引用,则会更加直观。但是,多路广播设计之所以使用前一个委托的概念,在于执行处理程序方法的序列。原因是每个委托对象跟踪前一个委托,本专栏将在后面部分对此进行更详细的说明。

在您创建了包含多个处理程序方法的多路广播时,列表头部的委托保留了一个对前一个委托的引用。该委托也保留了一个对前一个委托的引用。对于在列表尾部的委托对象,前一个委托字段将具有 Nothing 值。如您所见,委托对象在列表中所处的位置非常重要。

在调用 Combine 时,它将两个或多个委托对象链接到一起,并返回一个对列表头部委托对象的引用。请注意,前一个示例调用了接受两个委托参数的Combine 的重载实现。这种 Combine实现会创建一个多路广播列表,该列表将作为第二个参数传递的委托对象置于其头部。例如,在前面的代码示例中,位于列表头部的委托被绑定到LogWithdrawToDB方法。这个委托对象包含一个引用前一个委托对象(该对象被绑定到 GetApproval 方法)的私有字段。

请注意,此处还有另外一个可接受委托对象数组的 Combine 的重载版本。在将委托对象数组传递到 Combine 方法时,该方法将数组中 0位置处的第一个委托对象放到列表的头部。您可以按照喜好调用任意一个 Combine重载版本。但是要注意每个委托对象是如何放入到列表中的。您可以控制首先执行哪个处理程序方法。

在对列表头部的委托对象调用Invoke 时,MulticastDelegate 类提供通过列表枚举和执行链中每个委托对象的 Invoke方法所需的代码。请注意单个处理程序方法以序列化和同步方式继续执行,这一点非常重要。还应注意首先执行哪一个处理程序方法。

在对前一个委托调用 Invoke方法之前,多路广播列表头部的委托对象不会执行其处理程序方法。这就是多路广播委托设计模式将其称为前一个委托对象,而不是后一个委托对象的原因。您应该看到,在执行任何处理程序方法之前,控制是从列表头部的委托对象传递至列表尾部的委托对象的。

多路广播列表尾部的委托对象始终首先执行其处理程序方法。因此,执行顺序总是从后向前。您应该看到,多路广播列表头部的委托对象始终最后执行其处理程序。在我的示例中涉及到了两个委托对象的多路广播,先执行 GetApproval 方法,然后执行 LogWithdrawToDB 方法。

使用多路广播回调

现在,让我们回顾一下涉及 BankAccount 类的示例并添加对多路广播的支持。我将修改类实现,以便 BankAccount 对象可对处理程序方法列表进行回调。请参见 图 1 中的类定义。

首先请注意,处理程序字段已经重命名为“handlers”,表示该字段可用于保留一个对多路广播委托对象的引用。但是,这只是一个重命名问题,因为该字段仍然基于 LargeWithdrawHandler 委托类型。

RegisterHandler方法的实现已经进行了更新,以便支持多路广播。现在,RegisterHandler 的实现调用 Combine方法向现有的委托对象列表中添加新的委托对象。请注意,RegisterHandler 的实现在其调用中将新的委托对象作为第二个参数传递给Combine。这意味着新的委托对象将成为列表头,因此,在链中将最后一个执行它。如果您愿意,可以简单地重写RegisterHandler,以便将新的处理程序方法放入多路广播列表的尾部。在多路广播列表中,将先执行新的处理程序方法,然后再执行之前注册的任意处理程序:

Sub RegisterHandler(ByVal handler As LargeWithdrawHandler)
'*** add new handler to tail of multicast list
Dim NewList As [Delegate] = [Delegate].Combine(handler, handlers)
handlers = CType(NewList, LargeWithdrawHandler)
End Sub

您也许还注意到,除了将处理程序字段名更改为“handlers”之外,Withdraw 方法并不要求任何其他修改。Invoke的调用方法与以前介绍的完全相同。这阐释了使用多路广播委托的最有价值的一个方面。多路广播并不需要关心被绑定到委托对象的目标方法的数量。通知源只调用Invoke,而且每个处理程序方法自动执行。

既然 BankAccount 类已经更新为支持多路广播,那么就可重写应用程序,注册三个不同的处理程序方法,以便对大额提款通知做出响应,如以下代码所示:

Sub Main()
'*** create bank account object
Dim acc1 As New BankAccount()
'*** create register handler methods
acc1.RegisterHandler(AddressOf AccountHandlers.GetApproval)
acc1.RegisterHandler(AddressOf AccountHandlers.LogWithdrawToDB)
acc1.RegisterHandler(AddressOf AccountHandlers.LogWithdrawToFile)
'*** do something that triggers callback
acc1.Withdraw(5001)
End Sub

您应该从这个设计中看到,多路广播允许排列处理程序方法,使它们按可预知的系列执行。图 2显示了如何进行设计的高级视图。由于 GetApproval 方法被放置于列表的尾部,因此它将首先执行。然后执行LogWithdrawFromDB 方法。由于 LogWithdrawToFile 方法最后一个注册,因此,它将被放置于列表的头部并在最后执行。

fig02
 

图 2 多路广播委托执行处理程序

图 3 显示了一个基于我刚才预排设计的完整的应用程序。在继续前进之前,您应当理解此处使用委托创建的内容:一个松散的耦合设计,用于实现支持多路广播的回调。通过创建更多的处理程序方法并使用委托对它们进行注册,可以轻松地对这个应用程序进行进一步的自定义。

调用 GetInvocationList 方法

在许多情况下,通知源只需调用 Invoke来执行与多路广播委托对象相关的所有目标处理程序方法。但是,有些时候也需要更多控制。例如,您可能需要确定已经添加到多路广播委托列表中的目标处理程序方法的数量。还可能会要求您编写一些代码,以适当处理由列表中处理程序方法引发的异常。

Delegate 类提供了一个名为 GetInvocationList 的公共实例方法。当在多路广播委托之上调用此方法时,它会返回一个指向单个委托对象的引用数组。此数组使确定目前被绑定到一个多路广播委托的处理程序方法的数量成为可能:

'*** determine number of target handler methods
Dim HandlerCount As Integer = handlers.GetInvocationList().Length

对 GetInvocationList的调用也使通过单个委托对象枚举和同时准确地执行相对应的处理程序方法变得相对简单。由于调用 GetInvocationList会返回一个指向委托对象的引用数组,因此使得通过使用 For Each 循环的委托对象来构造枚举代码就更为简单:

Sub Withdraw(ByVal Amount As Decimal)
'*** send notifications if required
If Not (Amount > 5000) AndAlso (Not handlers Is Nothing) Then
Dim handler As LargeWithdrawHandler
For Each handler In handlers.GetInvocationList()
handler.Invoke(Amount)
Next
End If
'*** perform withdrawal
End Sub

与对处理程序字段执行 Invoke方法相比,您刚才看到的代码实际上并没有提供更多控制。因此,您可能会问,为什么时常需要通过列表中的委托对象调用GetInvocationList 进行枚举。一个原因是,如果在执行期间,某个处理程序方法引发一个异常,则您可能需要更多的控制。

让我们看一个示例。假设您持有一个被绑定到 10 个处理程序方法的多路广播委托对象。如果调用 Invoke时,第七个处理程序方法引发一个异常,情况会怎么样?前面六个处理程序方法都已经成功地执行。由第七个处理程序方法引发的异常会导致 Invoke方法意外终止。因此,根本就不会执行第八、第九和第十个处理程序方法。

问题在于,实际上无法知道成功地执行了哪一个处理程序方法、哪一个处理程序方法失败以及哪一个处理程序方法从未执行。如果重新构造代码,对 Try 块中的每个委托对象调用 Invoke 方法,就需要更多的控制:

Dim handler As LargeWithdrawHandler
For Each handler In handlers.GetInvocationList()
Try
handler.Invoke(Amount)
Catch ex As Exception
'*** deal with exception and continue
End Try
Next

您可能需要调用 GetInvocationList并分别通过每个委托对象枚举的另外一个原因是,它可从多个处理程序方法中检索返回值和输出参数。如果在多路广播委托对象上调用Invoke,并且它包含一个输出参数或返回值,则您获得的结果会有一点随机性。您接受的输出参数和返回值正是由最后执行的处理程序方法提供。请记住,最后执行的就是列表头部的委托对象。但是,如果编写额外的代码枚举整个列表并准确地对每个委托对象调用Invoke,则您可以捕获每个处理程序方法的独立输出参数和返回值。

请注意,GetInvocationList及时返回一个表示快照的数组。换句话说,GetInvocationList返回调用该方法时出现的列表委托对象。如果向多路广播中添加新的委托对象,随早先 GetInvocationList调用产生的数组将不会进行同步。您必须再次调用 GetInvocationList 以创建一个表示委托对象更新列表的新数组。

小结

至此,有关使用委托进行编程的两部分讨论均已完成。正如您所见,委托就是一种可编程的绑定机制,用于实现通知源和一个或多个处理程序方法之间的回调。委托为实现回调提供了一种充满魅力的方法,因为它们将接口的类型安全和多态能力与函数指针的效率和灵活性相结合。

掌握使用委托进行编程的基本技术是成为一名 Visual Basic .NET高级用户所需的必备知识。我这样说有两个理由。第一,.NETFramework 中的事件处理完全基于各种委托。如果您真的希望充分使用事件驱动的应用程序框架(如 Windows® 窗体或ASP.NET),您最好从基本技术着手准备,并在需要时使用委托进行编程。委托还提供了以异步方式在二级线程上执行方法的基本手段。

将您的问题与建议发送给 Ted instinct@microsoft.com

Ted Pattison是 DevelopMentor 的一名讲师和研究员,(http://www.develop.com),他在那与人共同教授 Visual Basic 课程。他是 Programming Distributed Applications with COM and Microsoft Visual Basic 6.0 (Microsoft Press, 2000)一书的作者。

转到原英文页面