深入理解委托与事件:原理、应用与设计模式
委托概述
委托是一种内置的、受语言支持的机制,用于定义和执行回调。它的灵活性允许我们精确地定义回调的签名,而这些信息会成为委托类型的一部分。在 Visual Basic(VB)和 .NET 平台中,委托是实现回调的首选方法。
当在代码中声明一个委托时,VB 编译器会生成一个从
MulticastDelegate
派生的类,而公共语言运行时(CLR)会在运行时动态实现委托的所有有趣方法。委托包含两个有用的字段:一个持有对象的引用,另一个持有方法指针。当调用委托时,会在包含的引用上调用实例方法;如果对象引用为
Nothing
,运行时会将其理解为该方法是一个共享方法。一个委托类型可以处理对实例方法或共享方法的回调,并且调用委托的语法与调用常规函数完全相同,这使得委托非常适合实现回调。
委托提供了一种出色的机制,用于将在实例上调用的方法与实际调用者解耦。调用者无需知道它调用的是实例方法还是共享方法,也无需知道具体调用的是哪个实例。例如,在对话框中的 UI 元素(如提交按钮),如果按钮类必须直接调用感兴趣的各方,会增加大量的耦合和复杂性。而委托可以打破这种联系,感兴趣的各方只需向按钮注册一个预配置为调用他们想要的任何方法的委托。
委托的创建和使用
委托声明与方法声明非常相似,只是多了一个
Delegate
关键字。以下是一个有效的委托声明示例:
Public Delegate Function ProcessResults(ByVal x As Double, ByVal y As Double) _
As Double
当编译器遇到这行代码时,会定义一个从
MulticastDelegate
派生的类型,并实现一个名为
Invoke
的方法,该方法的签名与委托声明中描述的方法完全相同。
在实例化委托时,必须将其连接到一个在调用时要执行的方法。这个方法可以是共享方法或实例方法,但其签名必须与委托兼容,即参数类型和返回类型必须与委托声明匹配,或者可以隐式转换为委托声明中的类型。
单委托示例
以下示例展示了如何创建委托:
Imports System
Public Delegate Function ProcessResults(ByVal x As Double, ByVal y As Double) _
As Double
Public Class Processor
Private factor As Double
Public Sub New(ByVal factor As Double)
Me.factor = factor
End Sub
Public Function Compute(ByVal x As Double, ByVal y As Double) As Double
Dim result As Double = (x + y) * factor
Console.WriteLine("InstanceResults: {0}", result)
Return result
End Function
Public Shared Function StaticCompute(ByVal x As Double, ByVal y As Double) _
As Double
Dim result As Double = (x + y) * 0.5
Console.WriteLine("StaticResult: {0}", result)
Return result
End Function
End Class
Public Class EntryPoint
Shared Sub Main()
Dim proc1 As Processor = New Processor(0.75)
Dim proc2 As Processor = New Processor(0.83)
Dim delegate1 As ProcessResults = _
New ProcessResults(AddressOf proc1.Compute)
Dim delegate2 As ProcessResults = _
New ProcessResults(AddressOf proc2.Compute)
Dim delegate3 As ProcessResults = _
New ProcessResults(AddressOf Processor.StaticCompute)
Dim combined As Double = _
delegate1(4, 5) + delegate2(6, 2) + delegate3(5, 2)
Console.WriteLine("Output: {0}", combined)
End Sub
End Class
运行上述代码会输出:
InstanceResults: 6.75
InstanceResults: 6.64
StaticResult: 3.5
Output: 16.89
这个示例创建了三个委托,其中两个指向实例方法,一个指向共享方法。通过创建
ProcessResults
类型的实例来创建委托,并在构造函数中传递要调用的方法。在调用委托时,语法是相同的,与委托指向的是实例方法还是共享方法无关。
委托链
委托链允许创建一个委托的链表,当调用链表头部的委托时,链中的所有委托都会被调用。
System.Delegate
类提供了一些共享方法来管理委托列表。以下是创建和管理委托列表的方法:
Public Class aDelegate
Implements ICloneable
Implements ISerializable
Public Shared Function Combine(ByVal Delegates As aDelegate()) _
As aDelegate
End Function
Public Shared Function Combine(ByVal First As aDelegate, _
ByVal Second As aDelegate) As aDelegate
End Function
Public Shared Function Remove(ByVal Source As aDelegate, _
ByVal Value As aDelegate) As aDelegate
End Function
Public Shared Function RemoveAll(ByVal Source As aDelegate, _
ByVal Value As aDelegate) As aDelegate
End Function
End Class
Combine
方法接受要组合的委托并返回另一个委托,返回的委托是
MulticastDelegate
的新实例,因为委托实例被视为不可变的。
Remove
方法移除源委托列表中最后一次出现的指定委托,
RemoveAll
方法移除源委托列表中所有出现的指定委托。
以下是一个修改后的示例,展示了如何组合委托:
Imports System
Public Delegate Function ProcessResults(ByVal x As Double, ByVal y As Double) _
As Double
Public Class Processor
Private factor As Double
Public Sub New(ByVal factor As Double)
Me.factor = factor
End Sub
Public Function Compute(ByVal x As Double, ByVal y As Double) As Double
Dim Result As Double = (x + y) * factor
Console.WriteLine("InstanceResults: {0}", Result)
Return Result
End Function
Public Shared Function StaticCompute(ByVal x As Double, ByVal y As Double) _
As Double
Dim Result As Double = (x + y) * 0.5
Console.WriteLine("StaticResult: {0}", Result)
Return Result
End Function
End Class
Public Class EntryPoint
Shared Sub Main()
Dim proc1 As Processor = New Processor(0.75)
Dim proc2 As Processor = New Processor(0.83)
Dim delegates As ProcessResults() = New ProcessResults() _
{New ProcessResults(AddressOf proc1.Compute), _
New ProcessResults(AddressOf proc2.Compute), _
New ProcessResults(AddressOf Processor.StaticCompute)}
Dim chained As ProcessResults = _
CType(System.Delegate.Combine(delegates), ProcessResults)
Dim combined As Double = chained(4, 5)
Console.WriteLine("Output: {0}", combined)
End Sub
End Class
运行上述代码会输出:
InstanceResults: 6.75
InstanceResults: 7.47
StaticResult: 4.5
Output: 4.5
这个示例将多个委托链在一起,然后通过调用链的头部来调用它们。需要注意的是,链式调用的结果是最后一个被调用的委托的结果,链中其他委托的返回值会被丢弃。如果任何一个委托抛出异常,委托链的处理将终止,CLR 将开始在栈上搜索下一个异常处理框架。在调用委托链之前,必须将委托转换回显式的委托类型,因为
Combine
和
Remove
方法返回的类型是
System.Delegate
,编译器需要知道如何调用委托。
遍历委托链
有时需要调用委托链,并获取每个调用的返回值,或者指定链中调用的顺序。
System.Delegate
类型提供了
GetInvocationList
方法,用于获取一个委托数组,数组中的每个元素对应于调用列表中的一个委托。以下是一个示例,展示了如何显式调用委托链中的每个委托:
Imports System
Public Delegate Function ProcessResults(ByVal x As Double, ByVal y As Double) _
As Double
Public Class Processor
Private factor As Double
Public Sub New(ByVal factor As Double)
Me.factor = factor
End Sub
Public Function Compute(ByVal x As Double, ByVal y As Double) As Double
Dim Result As Double = (x + y) * factor
Console.WriteLine("InstanceResults: {0}", Result)
Return Result
End Function
Public Shared Function StaticCompute(ByVal x As Double, ByVal y As Double) _
As Double
Dim Result As Double = (x + y) * 0.5
Console.WriteLine("StaticResult: {0}", Result)
Return Result
End Function
End Class
Public Class EntryPoint
Shared Sub Main()
Dim proc1 As Processor = New Processor(0.75)
Dim proc2 As Processor = New Processor(0.83)
Dim delegates As ProcessResults() = New ProcessResults() _
{New ProcessResults(AddressOf proc1.Compute), _
New ProcessResults(AddressOf proc2.Compute), _
New ProcessResults(AddressOf Processor.StaticCompute)}
Dim chained As ProcessResults = _
CType(System.Delegate.Combine(delegates), ProcessResults)
Dim chain As System.Delegate() = chained.GetInvocationList()
Dim accumulator As Double = 0
For i As Integer = 0 To chain.Length - 1
Dim current As ProcessResults = CType(chain(i), ProcessResults)
accumulator += current(4, 5)
Next i
Console.WriteLine("Output: {0}", accumulator)
End Sub
End Class
运行上述代码会输出:
InstanceResults: 6.75
InstanceResults: 7.47
StaticResult: 4.5
Output: 18.72
通过遍历委托链中的每个委托,可以获取每个委托的返回值,并进行相应的处理。
开放实例委托
前面的委托示例展示了如何将委托连接到特定类型的共享方法或特定实例的实例方法。但如果想让一个委托表示一个实例方法,并在一组实例上调用该委托,就需要使用开放实例委托。
当调用实例上的方法时,参数列表开头有一个隐藏的参数
Me
表示当前实例。在将封闭实例委托连接到对象实例上的实例方法时,委托在调用实例方法时会将对象实例作为
Me
引用传递。而开放实例委托会将这个操作推迟到调用委托的人,因此可以在委托调用时提供要调用的对象实例。
以下是一个示例,展示了如何创建和使用开放实例委托:
Imports System
Imports System.Reflection
Imports System.Collections.Generic
Delegate Sub ApplyRaiseDelegate(ByVal emp As Employee, _
ByVal percent As Decimal)
Public Class Employee
Private mSalary As Decimal
Public Sub New(ByVal salary As Decimal)
Me.mSalary = salary
End Sub
Public ReadOnly Property Salary() As Decimal
Get
Return mSalary
End Get
End Property
Public Sub ApplyRaiseOf(ByVal percent As Decimal)
mSalary *= 1 + percent
End Sub
End Class
Public Class EntryPoint
Shared Sub Main()
Dim Employees As List(Of Employee) = New List(Of Employee)
Employees.Add(New Employee(40000))
Employees.Add(New Employee(65000))
Employees.Add(New Employee(95000))
'Create open-instance delegate
Dim mi As MethodInfo = GetType(Employee).GetMethod("ApplyRaiseOf", _
BindingFlags.Public Or BindingFlags.Instance)
Dim applyRaise As ApplyRaiseDelegate = _
CType(System.Delegate.CreateDelegate(GetType(ApplyRaiseDelegate), _
mi), ApplyRaiseDelegate)
'Apply raise.
Dim e As Employee
For Each e In Employees
applyRaise(e, CType(0.1, Decimal))
'Send new salary to console.
Console.WriteLine("Employee's new salary = {0:C}", e.Salary)
Next
End Sub
End Class
运行上述代码会输出:
Employee's new salary = $44,000.00
Employee's new salary = $71,500.00
Employee's new salary = $104,500.00
在这个示例中,委托的声明在参数列表开头声明了
Employee
类型,这是为了暴露隐藏的实例指针,以便稍后进行绑定。不幸的是,VB 没有创建开放实例委托的特殊语法,因此必须使用更通用的
Delegate.CreateDelegate()
重载方法来创建委托实例,并使用反射来获取要绑定的方法的
MethodInfo
实例。
还可以使用泛型委托来声明委托,使得在声明时不指定要调用的类型,只有在实例化委托时才需要提供具体的类型。以下是一个修改后的示例:
Imports System
Imports System.Reflection
Imports System.Collections.Generic
Delegate Sub ApplyRaiseDelegate(Of T)(ByVal instance As T, _
ByVal percent As Decimal)
Public Class Employee
Private mSalary As Decimal
Public Sub New(ByVal salary As Decimal)
Me.mSalary = salary
End Sub
Public ReadOnly Property Salary() As Decimal
Get
Return mSalary
End Get
End Property
Public Sub ApplyRaiseOf(ByVal percent As Decimal)
mSalary *= 1 + percent
End Sub
End Class
Public Class EntryPoint
Shared Sub Main()
Dim Employees As List(Of Employee) = New List(Of Employee)
Employees.Add(New Employee(40000))
Employees.Add(New Employee(65000))
Employees.Add(New Employee(95000))
'Create open-instance delegate
Dim mi As MethodInfo = GetType(Employee).GetMethod("ApplyRaiseOf", _
BindingFlags.Public Or BindingFlags.Instance)
Dim applyRaise As ApplyRaiseDelegate(Of Employee) = _
CType([Delegate].CreateDelegate( _
GetType(ApplyRaiseDelegate(Of Employee)), mi), _
ApplyRaiseDelegate(Of Employee))
'Apply raise.
Dim e As Employee
For Each e In Employees
applyRaise(e, CType(0.1, Decimal))
'Send new salary to console.
Console.WriteLine("Employee's new salary = {0:C}", e.Salary)
Next
End Sub
End Class
这样的委托更加通用,在某些情况下非常有用,例如在图像处理程序中表示通用的过滤器类型。
策略模式
委托提供了一种方便的机制来实现策略模式。策略模式允许根据运行时情况动态交换计算算法。例如,在对一组项目进行排序时,可能希望尽可能快地进行排序,但这可能需要更多的临时内存。对于较小的集合,这种方法效果很好,但对于非常大的集合,快速排序所需的内存可能会超过系统资源容量。在这种情况下,可以提供一种速度较慢但使用资源少得多的排序算法。策略模式允许根据条件在运行时交换这些算法。
通常使用接口来实现策略模式,声明一个所有策略实现都要实现的接口,算法的使用者不需要关心使用的是哪个具体的策略实现。而委托提供了一种更轻量级的替代方案,用委托声明来实现契约,任何匹配委托签名的方法都是潜在的具体策略。以下是一个示例,展示了如何使用委托实现策略模式:
Imports System
Imports System.Collections
Public Delegate Function SortStrategy(ByVal theCollection As ICollection) As Array
Public Class Consumer
Private myCollection As ArrayList
Private mStrategy As SortStrategy
Public Sub New(ByVal defaultStrategy As SortStrategy)
Me.mStrategy = defaultStrategy
End Sub
Public Property Strategy() As SortStrategy
Get
Return mStrategy
End Get
Set(ByVal value As SortStrategy)
mStrategy = value
End Set
End Property
Public Sub DoSomeWork()
'Employ the strategy.
Dim sorted As Array = mStrategy(myCollection)
'Do something with the results.
End Sub
End Class
Public Class SortAlgorithms
Private Shared Function SortFast(ByVal theCollection As ICollection) As Array
'Do the fast sort.
End Function
Private Shared Function SortSlow(ByVal theCollection As ICollection) As Array
'Do the slow sort.
End Function
End Class
在这个示例中,
Consumer
对象在实例化时会传递一个默认的排序策略,该策略是一个实现了
SortStrategy
委托签名的方法。如果运行时条件合适,可以交换排序策略,
Consumer.DoSomeWork
方法会自动调用替换后的策略。使用委托实现策略模式比使用接口更灵活,因为委托可以绑定到共享方法和实例方法。
事件
在很多情况下,使用委托作为回调机制时,可能希望通知某人某个事件发生了,例如 UI 中的按钮按下事件。在设计媒体播放器应用程序时,UI 和控制逻辑通常通过桥接模式进行分离,以实现解耦。委托是定义这种接口的优秀机制,核心系统不需要关心用户如何向 UI 表示要开始播放媒体,只要遵循相同的接口契约即可。
.NET 运行时定义了一种形式化的内置事件机制。在类中声明事件时,编译器会实现一些隐藏方法,允许注册和注销在特定事件发生时被调用的委托。事件实际上是一种快捷方式,节省了编写管理委托链的注册和注销方法的时间。
以下是一个简单的事件示例:
Imports System
'Arguments passed from UI when play event occurs.
Public Class PlayEventArgs
Inherits EventArgs
Private mFilename As String
Public Sub New(ByVal filename As String)
Me.mFilename = filename
End Sub
Public ReadOnly Property Filename() As String
Get
Return mFilename
End Get
End Property
End Class
Public Class PlayerUI
'Define event for play notifications.
Public Event PlayEvent As EventHandler(Of PlayEventArgs)
Public Sub UserPressedPlay()
OnPlay()
End Sub
Protected Overridable Sub OnPlay()
'Fire the event.
Dim localHandler As EventHandler(Of PlayEventArgs) = PlayEventEvent
If Not localHandler Is Nothing Then
localHandler(Me, New PlayEventArgs("song.wav"))
End If
End Sub
End Class
Public Class CorePlayer
Private ui As PlayerUI
Public Sub New()
ui = New PlayerUI()
'Register our event handler.
AddHandler ui.PlayEvent, AddressOf PlaySomething
End Sub
Private Sub PlaySomething(ByVal source As Object, ByVal args As PlayEventArgs)
'Play the file.
End Sub
End Class
Public Class EntryPoint
Shared Sub Main()
Dim player As CorePlayer = New CorePlayer()
End Sub
End Class
事件对委托的使用有一定的规则,委托不能返回任何值,并且必须接受两个参数:一个表示生成事件的对象引用,另一个必须是从
System.EventArgs
派生的类型。在
PlayerUI
类中,
PlayEvent
事件在事件生成方的使用方式类似于委托,在
OnPlay
方法中调用它来通知所有注册的监听器。在事件消费方,使用
AddHandler
语句来注册事件监听器。
自定义事件
VB 2005 为
Event
语句添加了一个新的关键字
Custom
,自定义事件允许在代码添加/移除事件处理程序或引发事件时指定操作。通过修改
AddHandler
、
RemoveHandler
和
RaiseEvent
访问器来实现。以下是一个修改后的
PlayerUI
类示例:
Public Class PlayerUI
'Define event for play notifications.
Private PlayEventEvent As EventHandler(Of PlayEventArgs)
Public Custom Event PlayEvent As EventHandler(Of PlayEventArgs)
AddHandler(ByVal value As EventHandler(Of PlayEventArgs))
PlayEventEvent = _
CType(System.Delegate.Combine(PlayEventEvent, value), _
EventHandler(Of PlayEventArgs))
End AddHandler
RemoveHandler(ByVal value As EventHandler(Of PlayEventArgs))
PlayEventEvent = _
CType(System.Delegate.Remove(PlayEventEvent, value), _
EventHandler(Of PlayEventArgs))
End RemoveHandler
RaiseEvent(ByVal sender As Object, ByVal e As PlayEventArgs)
If Not sender Is Nothing Then
'Add event code here.
End If
End RaiseEvent
End Event
Public Sub UserPressedPlay()
OnPlay()
End Sub
Protected Overridable Sub OnPlay()
'Fire the event.
Dim localHandler As EventHandler(Of PlayEventArgs) = PlayEventEvent
If Not localHandler Is Nothing Then
localHandler(Me, New PlayEventArgs("song.wav"))
End If
End Sub
End Class
在事件声明的
AddHandler
和
RemoveHandler
部分,通过
value
关键字引用要添加或移除的委托。这个示例使用
Delegate.Combine()
和
Delegate.Remove()
来管理内部委托链。
事件非常适合实现发布 - 订阅设计模式和观察者模式,许多监听器可以注册以接收事件通知。
总结
委托提供了一种一流的、系统定义和实现的机制,用于统一表示回调。通过不同的方式可以声明和创建不同类型的委托,包括单委托、委托链和开放实例委托。委托还可以作为事件的构建块,并且可以用于实现各种设计模式,因为委托是定义编程契约的好方法,而几乎所有设计模式的核心都是一个明确定义的契约。
深入理解委托与事件:原理、应用与设计模式
委托与事件的应用场景总结
委托和事件在软件开发中有着广泛的应用场景,下面通过表格的形式进行总结:
|应用场景|描述|示例代码或说明|
| ---- | ---- | ---- |
|UI交互|处理用户在界面上的操作,如按钮点击、菜单选择等|在前面的媒体播放器示例中,通过事件处理用户点击播放按钮的操作|
|算法切换|根据运行时情况动态切换算法,实现策略模式|排序时根据集合大小选择不同的排序算法,使用委托实现策略模式的示例代码|
|数据处理|对一组数据进行不同方式的处理|可以定义不同的委托来实现对数据的筛选、排序、计算等操作|
|发布 - 订阅模式|多个对象可以订阅某个事件的通知|事件机制本身就是发布 - 订阅模式的一种实现,多个监听器可以注册到一个事件上|
|观察者模式|当一个对象的状态发生变化时,通知其他依赖对象|可以使用事件来实现观察者模式,当某个对象的属性改变时触发事件通知观察者|
委托和事件的性能考虑
在使用委托和事件时,也需要考虑一些性能方面的因素,以下是一些关键点:
-
委托调用开销
:每次调用委托都有一定的开销,包括方法查找和调用的开销。如果在性能敏感的代码中频繁调用委托,可能会影响性能。可以通过减少不必要的委托调用或者使用内联代码来优化。
-
委托链的性能
:委托链中包含多个委托时,调用委托链会依次调用链中的每个委托。如果委托链过长,会增加调用的时间和资源消耗。在使用委托链时,需要根据实际情况合理控制链的长度。
-
事件注册和注销
:事件的注册和注销操作也有一定的开销,特别是在频繁注册和注销的情况下。尽量避免在循环中频繁进行事件的注册和注销操作。
委托和事件的最佳实践
为了更好地使用委托和事件,以下是一些最佳实践建议:
-
明确委托和事件的用途
:在使用委托和事件之前,要清楚它们的用途和适用场景,避免滥用。例如,只有在需要实现回调、解耦或者实现设计模式时才使用委托和事件。
-
合理设计委托签名
:委托的签名应该简洁明了,只包含必要的参数和返回值。避免使用过于复杂的委托签名,以免增加代码的复杂度和维护难度。
-
异常处理
:在委托和事件的使用中,要考虑异常处理。如果委托链中的某个委托抛出异常,可能会导致整个委托链的处理终止。可以在委托调用时添加异常处理代码,或者在事件处理方法中进行异常处理。
-
线程安全
:在多线程环境中使用委托和事件时,要注意线程安全问题。例如,在事件触发时,可能会有多个线程同时访问事件的委托链,需要进行适当的同步操作,避免出现竞态条件。
委托和事件的未来发展趋势
随着软件开发技术的不断发展,委托和事件也在不断演进和改进。以下是一些可能的未来发展趋势:
-
与异步编程的结合
:随着异步编程的普及,委托和事件可能会更多地与异步编程模型结合,例如使用异步委托和事件来处理异步操作,提高程序的性能和响应性。
-
更强大的类型系统支持
:未来的编程语言可能会提供更强大的类型系统支持,使得委托和事件的使用更加安全和方便。例如,更好地支持泛型委托和事件,减少类型转换的需求。
-
与新兴技术的融合
:随着人工智能、机器学习等新兴技术的发展,委托和事件可能会与这些技术进行融合,例如在机器学习模型的训练和预测过程中使用委托和事件来处理回调和通知。
总结与展望
委托和事件是软件开发中非常重要的概念,它们提供了一种灵活、高效的方式来实现回调、解耦和设计模式。通过本文的介绍,我们了解了委托的创建和使用、委托链的管理、开放实例委托的应用、策略模式的实现以及事件的声明和处理等内容。
在实际开发中,我们应该根据具体的需求和场景合理使用委托和事件,遵循最佳实践,考虑性能和线程安全等因素。同时,我们也应该关注委托和事件的未来发展趋势,不断学习和掌握新的技术和方法,以提高我们的软件开发能力。
希望本文能够帮助你深入理解委托和事件,并在实际项目中更好地应用它们。如果你对委托和事件还有其他疑问或者想要进一步探讨相关话题,欢迎在评论区留言。
流程图
graph TD;
A[开始] --> B[定义委托];
B --> C[创建委托实例];
C --> D{委托类型};
D -- 单委托 --> E[调用委托];
D -- 委托链 --> F[组合委托];
F --> G[调用委托链];
D -- 开放实例委托 --> H[创建开放实例委托];
H --> I[调用开放实例委托];
E --> J[处理返回值];
G --> K[处理最后一个委托的返回值];
I --> L[处理每个实例的操作结果];
J --> M[结束];
K --> M;
L --> M;
这个流程图展示了委托的主要使用流程,包括定义委托、创建委托实例、根据委托类型进行不同的操作(单委托、委托链、开放实例委托)以及处理返回值,最后结束流程。通过这个流程图,可以更直观地理解委托的使用过程。
超级会员免费看
3万+

被折叠的 条评论
为什么被折叠?



