Programming .NET Components 2nd 学习笔记(九)
2012-07-22 16:57
323 查看
6.2. Working with .NET Events
本节将讨论.NET事件设计准则及开发实践,促进发布服务器和订阅服务器之间的松散耦合,提高可用性,符合现有的约定,并从.NET丰富的事件支持框架获益。另一个事件相关的技术(异步发布事件)将在第7章讨论。
首先,目标方法应该用于void返回类型。例如,一个处理数的新值的事件,它的签名可能是:
你应该使用void返回类型的原因是向事件发布服务器返回一个值是无意义的。事件发布服务器该怎样处理这些值?首先,发布服务器不知道订阅服务器订阅该事件的原因。此外,委托类对发布服务器隐藏了实际发布的动作。委托会遍历接收服务器(订阅了事件的对象)列表,调用每个对应的方法;这些返回值不会传递给发布服务器的代码。使用void返回类型的逻辑也表明你应该避免使用输出参数,因为这些输出参数也不会传递给发布服务器。
第二,一些订阅服务器可能想从多个事件发布源处接收相同的事件。因为订阅服务器不能灵敏地定义和发布服务器数量一样的方法,所以订阅服务器会想向多个发布服务器提供同一个方法。为了使订阅服务器能区别是哪个发布服务器触发的事件,方法签名应该包含发布服务器的身份。不依靠于泛型(待会讨论),最简单的方法是添加一个叫做sender的object类型参数:
接下来订阅服务器只需将自己做为sender传递(在C#中使用this)。
最后,由于订阅服务器需要一系列参数,我们要定义一个实际的事件参数(例如int number)连接发布服务器与订阅服务器。如果你以后想要改变参数时,只需改变这个参数就能影响所有订阅服务器。为了隐藏参数改变的影响,.NET提供了一个权威的事件参数容器,EventArgs类,你可以使用它替代一系列特定的参数。EventArgs类定义如下:
替代特定的事件参数,你可以传递一个EventArgs对象:
如果发布服务器不需要参数,简单地传递EventArgs.Empty,从静态构造函数和静态只读成员Empty中获益。
如果事件需要参数,就从EventArgs派生一个类,例如NumberChangedEventArgs;添加需要的成员变量、方法或者属性;并通过该派生类传递。订阅服务器需要将EventArgs转换为事件关联的特定参数类(例如NumberChangedEventArgs),然后获取参数。例子6-5展示EventArgs派生类的用法。
Example 6-5. Events arguments using an EventArgs-derived class
从EventArgs派生类来传递特定参数允许你添加参数,移除未使用的参数,从另外一个继承于EventArgs的类派生类等等,不会强迫那些不关系新变化的订阅服务器变化。
因为结果委托定义非常多,所以.NET提供了EventHandler委托:
EventHandler广泛应用在.NET应用程序框架中,例如Windows Forms和ASP.NET。然而,无定形的基类所带来的灵活性带来了昂贵的类型安全开销。为了解决这个问题,.NET提供了EventHandler委托的泛型版本:
如果你所有的事件形式都是一个object类型的sender和一个EventArgs派生类,那么你能通过EventHandler泛型委托获得好处。其他情况下,你仍然需要定义委托来处理特殊的签名。
警告:订阅服务器的事件处理方法的命名约定是On<EventName>,这能是代码更标准和可读。
Example 6-6. Preventing subscribers from modifyingparameters in the argument class
警告:GenericEventHandler中使用的这种技术叫做重载,通过类型参数数量来识别。实际上,编译器会赋给重载委托不同的名字,基于参数数量或泛型类型来区分它们。举例来说:
会显示:
因为编译器委托的名字上添加了`2(2是泛型类型参数的数量)。
这些不同版本的GenericEventHandler可以用来调用任何参数在0到7之间的事件处理方法(多于5个参数是个坏的实践,应该使用结构体或者EventArgs派生类来封装这些参数)。使用GenericEventHandler时可以定义任何类型的组合。例如:
或者,为了传递多个参数:
例子6-7展示了GenericEventHandler和泛型事件处理方法的用法。
这个例子使用了两个类型参数的GenericEventHandler,sender类型和容器参数类,与泛型、非泛型的EventHandler非常相似。然而,不像EventHandler,GenericEventHandler是类型安全的,因为它只接受MyPublisher类型的对象(而不仅仅是对象就可以)作为sender。明显地,你可以使用GenericEventHandler处理所有的事件签名,包括那些参数不是sender对象和EventArgs的签名。
为了示范,例子6-7使用了泛型订阅服务器,接收一个泛型类型参数用作事件参数容器。你也可以用特定类型来替代它,而不影响发布服务器的代码:
如果你想要强制使用一个EventArgs派生类作为参数,你可以在GenericEventHandler上加个约束:
然而,为了更广泛地使用GenericEventHandler,最好将约束放在订阅类型上(或者订阅方法),就像例子6-7那样。
警告:有时候对特定类型使用别名十分有用。你可以通过using语句来做:
注意,别名的作用范围是在该文件范围,所以你需要在项目中重复该别名,就像使用命名空间一样。
基于委托的事件的另一个问题在于异常。任何订阅服务器未处理的异常将会传播给发布服务器。一些订阅服务器可能会在处理事件时遇到异常,但没有处理它,这将导致发布服务器挂掉。因此,你应该始终在发布事件时使用try/catch块。例子6-8展示了这些观点。
Example 6-8. Defensive publishing
然而,例子6-8中的代码因为一个订阅服务器抛出了异常而中止了事件发布。有时候你想要在一个订阅服务器抛出异常之后继续发布事件。为了达到这个目的,你需要手动地遍历完委托内部的列表,捕捉列表中委托抛出的任何异常。你可以通过使用一个每个委托都支持的特殊方法来获取内部列表,这个方法叫做GetInvocationList()。这个方法的定义如下:
GetInvocationList()返回一个可以遍历的委托集合,正如例子6-9.
Example 6-9. Continuous publishing in the face ofexceptions thrown by the subscribers
6.2.4.1 TheEventsHelper class
例子6-9中的问题在于代码不能重用,每次当你想要隔离发布服务器与订阅服务器的错误时就要复制这段代码。写一个帮助类来发布任何委托,传递任何参数集合,捕捉潜在的异常是可能的。例子6-10展示展示了EventsHelper静态类,它提供了静态方法Fire()。这个方法能防御性地出发任何类型的事件。
Example 6-10. The EventsHelper class
实现EventsHelper有两个关键元素。第一点时调用任何委托的能力。这通过使用DynamicInvoke()方法达到,每个委托都提供该方法。DynamicInvoke()通过传递一个参数集合来调用委托。定义如下:
第二点是将开放数量的对象作为参数传给订阅服务器。这通过C#params参数修饰符做到,允许一系列对象作为参数。编译器将这一系列参数转换为一个object数组,并传递该数组。使用EventsHelper是优雅且直接的:简单地传递要调用的委托和相关参数。例如,MyEventHandler,定义如下:
发布代码如下:
6.2.4.2 MakingEventsHelper type-safe
例子6-10展示的代码中的EventsHelper存在无类型安全的问题。Fire()方法有一个无组织对象的集合,它允许任何组合形式的参数,包括不正确的组合。例如,给出一个委托定义:
下面的发布代码能进行编译但会在发布的时候失败:
在编译时不会检测到参数数量或类型的不匹配。此外,EventsHelper会扼杀异常,也许你将不会注意到这个问题。
然而,如果你致力于使用GenericEventHandler替代定义自己的事件处理委托,有一种方法能强制编译时期类型安全。替代定义下面这个委托:
直接使用GenericEventHandler,或者给它别名:
接下来,结合EventsHelper和GenericEventHandler,正如例子6-11.
Example 6-11. The type-safe EventsHelper
因为传递给GenericEventHandler的参数数量与类型是已知的,所以可以在编译时保证类型安全。感谢通过泛型类型参数数量的重载,你甚至不用指定传递给Fire()方法的泛型类型参数。发布事件的代码和使用例子6-10的代码相同:
编译器推断出使用的类型参数的类型和数量,然后选择正确的Fire()重载版本。
如果你确保只使用EventHandler而不是GenericEventHandler,你可以在EventsHelper中添加这些重载方法:
这能使你防御性地发布事件,用一种类型安全的习惯,任何基于EventHandler的事件。
EventsHelper同样能提供非类型安全的UnsafeFire()方法:
这在处理不是基于GenericEventHandler或者EventHandler得委托时要用到,或者当你要处理比GenericEventHandler参数数量多的委托时。显然,应避免定义使用多于5个参数的方法,但至少使用EventsHelper的开发者注意到了类型安全陷阱。如果你想始终强制使用类型安全的Fire()方法,只需将UnsafeFire()改为私有即可:
6.2.5. Event Accessors
为了连接订阅服务器与发布服务器,你可以直接访问发布服务器的事件成员变量。将类成员暴露为公有是在寻找麻烦;它违背了面向对象设计的核心原则——封装和信息隐藏,并耦合了所有订阅服务器和确定的成员变量定义。为了减轻这个问题,C#提供了类似属性机制的事件访问器。访问器提供的好处和属性相似,隐藏实际类成员却维持了原本的易用性。C#使用add和remove执行+=和-=操作符,独自封装了事件成员变量。例子6-12演示了事件访问器的用法和相应的客户端代码。注意,当你使用事件访问器时,将已被封装的委托成员当做事件是没有好处的。
Example 6-12. Using event accessors
6.2.6. Managing Large Numbers of Events
想象一个发布了大量事件的类。这在开发框架时很常见;例如,在System.Windows.Forms命名空间中的Control类有非常多对于Windows消息的事件。处理大量事件的问题是为每个事件分配一个类成员是不可能的:类的定义,文档,实例工具视图,甚至智能感应都难以管理。为了解决该问题,.NET提供了EventHandlerList类(在System.ComponentModel命名空间):
EventHandlerList是一个存储键值对的线性列表。键是一个标识事件的对象,值是一个System.Delegate实例。因为索引是一个对象,它可以是一个整数、字符串、一个具体的按钮实例等等。通过使用AddHandler和RemoveHandler方法来添加和移除一个事件处理方法。你也可以使用AddHandlers()方法直接添加一个现有的EventHandlerList。要触发一个事件,需要通过键对象来索引得到事件列表,会得到一个System.Delegate对象。接下来需要将这个委托转换为实际的事件委托并触发事件。
例子6-13展示了如何使用EventHandlerList类,在实现一个类似Windows Forms按钮叫做MyButton的类。这个按钮支持很多事件,例如鼠标点击和鼠标移动,它们都通向同一个事件列表。使用事件访问器,将完全封装信息。
Example 6-13. Using the EventHandlerList class to manage a large number of events
例子6-13的问题是例如鼠标移动、鼠标点击等事件触发很频繁,每次调用新建一个字符串当做键会增加托管堆的压力。一个更好的方法是使用预分配的静态键,在所有实例中共享:
6.2.7. Writing Sink Interfaces
通过隐藏实际的事件成员,事件访问器提供勉强的封装性。然而,你可以增强这个模型。为了说明,考虑下面情况,一个订阅服务器希望订阅一系列事件。为什么它要进行多次可能开销昂贵的调用去设置和断开连接?为什么订阅服务器首先需要知道事件访问器?如果订阅服务器想要用接口代替方法来接收事件会怎么样?接下来的要提供一个简单却广泛的方式来管理发布服务器与订阅服务器之间的连接,这样能节约不必要的调用,封装事件访问器和成员,并允许订阅接口。本部分会描述我开发的用来实现这些的技术。考虑定义了一系列事件的一个接口,ImySubscriber接口:
任何人可以实现这个接口,并且所有的发布服务器应该知道它:
接下来,定义一个关于此事件的枚举,并用Flags特性标记:
Flags特性指出这个枚举的值可以用作位屏蔽(例如EventType.OnAllEvents的定义)。这允许你使用|按位操作符(按位或)来结合不同的枚举值或者使用&操作符(按位与)来屏蔽他们。
发布服务器提供两个方法,Subscribe()和Unsubscribe(),都接受两个参数:接口和一个位掩码标志指出接收服务器接口订阅的是哪个事件。发布服务器的每个方法可以在接收服务器内拥有一个相对应的事件委托成员,或者是一个对应所有方法的委托成员(这是被订阅服务器隐藏的实现细节)。例子6-14在接收服务器中为每个方法使用了一个事件成员变量。它展示了Subscribe()和Unsubscribe()和FireEvent()方法,为了清楚明了移除了错误处理代码。Subscribe()检查标志并订阅相应的接口方法:
UnSubscribe()使用同样风格来移除订阅。
订阅和取消订阅的代码都十分简单:
不过,它展示了一种优雅的方式通过一次调用订阅所有接口,同样也展示了封装事件的类的复杂性。
本节将讨论.NET事件设计准则及开发实践,促进发布服务器和订阅服务器之间的松散耦合,提高可用性,符合现有的约定,并从.NET丰富的事件支持框架获益。另一个事件相关的技术(异步发布事件)将在第7章讨论。
6.2.1. Defining Delegate Signatures
虽然在技术上一个委托声明可以定义任何方法签名,实际上,事件委托应该符合一些特殊的指导。首先,目标方法应该用于void返回类型。例如,一个处理数的新值的事件,它的签名可能是:
public delegate void NumberChangedEventHandler(int number);
你应该使用void返回类型的原因是向事件发布服务器返回一个值是无意义的。事件发布服务器该怎样处理这些值?首先,发布服务器不知道订阅服务器订阅该事件的原因。此外,委托类对发布服务器隐藏了实际发布的动作。委托会遍历接收服务器(订阅了事件的对象)列表,调用每个对应的方法;这些返回值不会传递给发布服务器的代码。使用void返回类型的逻辑也表明你应该避免使用输出参数,因为这些输出参数也不会传递给发布服务器。
第二,一些订阅服务器可能想从多个事件发布源处接收相同的事件。因为订阅服务器不能灵敏地定义和发布服务器数量一样的方法,所以订阅服务器会想向多个发布服务器提供同一个方法。为了使订阅服务器能区别是哪个发布服务器触发的事件,方法签名应该包含发布服务器的身份。不依靠于泛型(待会讨论),最简单的方法是添加一个叫做sender的object类型参数:
public delegate void NumberChangedEventHandler(object sender,int number);
接下来订阅服务器只需将自己做为sender传递(在C#中使用this)。
最后,由于订阅服务器需要一系列参数,我们要定义一个实际的事件参数(例如int number)连接发布服务器与订阅服务器。如果你以后想要改变参数时,只需改变这个参数就能影响所有订阅服务器。为了隐藏参数改变的影响,.NET提供了一个权威的事件参数容器,EventArgs类,你可以使用它替代一系列特定的参数。EventArgs类定义如下:
public class EventArgs { public static readonly EventArgs Empty; static EventArgs( ) { Empty = new EventArgs( ); } public EventArgs( ) {} }
替代特定的事件参数,你可以传递一个EventArgs对象:
public delegate void NumberChangedEventHandler(object sender,EventArgs eventArgs);
如果发布服务器不需要参数,简单地传递EventArgs.Empty,从静态构造函数和静态只读成员Empty中获益。
如果事件需要参数,就从EventArgs派生一个类,例如NumberChangedEventArgs;添加需要的成员变量、方法或者属性;并通过该派生类传递。订阅服务器需要将EventArgs转换为事件关联的特定参数类(例如NumberChangedEventArgs),然后获取参数。例子6-5展示EventArgs派生类的用法。
Example 6-5. Events arguments using an EventArgs-derived class
public delegate void NumberChangedEventHandler(object sender,EventArgs eventArgs);
public class NumberChangedEventArgs : EventArgs
{
public int Number;//This should really be a property
}
public class MyPublisher
{
public event NumberChangedEventHandler NumberChanged;
public void FireNewNumberEvent(EventArgs eventArgs)
{
//Always check delegate for null before invoking
if(NumberChanged != null)
NumberChanged(this,eventArgs);
}
}
public class MySubscriber
{
public void OnNumberChanged(object sender,EventArgs eventArgs)
{
NumberChangedEventArgs numberArg;
numberArg = eventArgs as NumberChangedEventArgs;
Debug.Assert(numberArg != null);
string message = numberArg.Number;
MessageBox.Show("The new number is "+ message);
}
}
//Client-side code
MyPublisher publisher = new MyPublisher( );
MySubscriber subscriber = new MySubscriber( );
publisher.NumberChanged += subscriber.OnNumberChanged;
NumberChangedEventArgs numberArg = new NumberChangedEventArgs( );
numberArg.Number = 4;
//Note that the publisher can publish without knowing the argument type
publisher.FireNewNumberEvent(numberArg);
从EventArgs派生类来传递特定参数允许你添加参数,移除未使用的参数,从另外一个继承于EventArgs的类派生类等等,不会强迫那些不关系新变化的订阅服务器变化。
因为结果委托定义非常多,所以.NET提供了EventHandler委托:
public delegate void EventHandler(object sender,EventArgs eventArgs);
EventHandler广泛应用在.NET应用程序框架中,例如Windows Forms和ASP.NET。然而,无定形的基类所带来的灵活性带来了昂贵的类型安全开销。为了解决这个问题,.NET提供了EventHandler委托的泛型版本:
public delegate void EventHandler<E>(object sender,E e) where E : EventArgs;
如果你所有的事件形式都是一个object类型的sender和一个EventArgs派生类,那么你能通过EventHandler泛型委托获得好处。其他情况下,你仍然需要定义委托来处理特殊的签名。
警告:订阅服务器的事件处理方法的命名约定是On<EventName>,这能是代码更标准和可读。
6.2.2. DefiningCustom Event Arguments
正如前面部分所说,你应将参数封装到EventArgs的派生类中然后提供给事件句柄。委托类简单地遍历订阅服务器列表,向每个订阅服务器传递参数对象。然而,并不能阻止一个订阅服务器去修改这些参数的值,这将会影响到所有处理该事件的订阅服务器。通常,你需要阻止订阅服务器修改这些参数的成员。为了阻止改变参数,这些参数只提供只读属性或提供公有只读字段。这两种情况下,需要在构造函数中初始化参数值。例子6-6展示了这两种技术。Example 6-6. Preventing subscribers from modifyingparameters in the argument class
public class NumberEventArgs1 : EventArgs { public readonly int Number; public NumberEventArgs1(int number) { Number = number; } } public class NumberEventArgs2 : EventArgs { int m_Number; public NumberEventArgs2(int number) { m_Number = number; } public int Number { get { return m_Number; } } }
6.2.3. TheGeneric Event Handler
泛型委托在处理事件时尤其有用。假设所有用于事件管理的委托返回void并没有输出参数,那么仅从参数数量及类型就能区别委托。这种区别能通过使用泛型轻易实现。考虑这一系列委托定义:public delegate void GenericEventHandler( ); public delegate void GenericEventHandler<T>(T t); public delegate void GenericEventHandler<T,U>(T t,U u); public delegate void GenericEventHandler<T,U,V>(T t,U u,V v); public delegate void GenericEventHandler<T,U,V,W>(T t,U u,V v,W w); public delegate void GenericEventHandler<T,U,V,W,X>(T t,U u,V v,W w,X x); public delegate void GenericEventHandler<T,U,V,W,X,Y>(T t,U u,V v,W w,X x,Y y); public delegate void GenericEventHandler<T,U,V,W,X,Y,Z>(T t,U u,V v,W w,X x,Y y,Z z);
警告:GenericEventHandler中使用的这种技术叫做重载,通过类型参数数量来识别。实际上,编译器会赋给重载委托不同的名字,基于参数数量或泛型类型来区分它们。举例来说:
Type type = typeof(GenericEventHandler<,>); Trace.WriteLine(type.ToString());
会显示:
GenericEventHandler`2[T,U]
因为编译器委托的名字上添加了`2(2是泛型类型参数的数量)。
这些不同版本的GenericEventHandler可以用来调用任何参数在0到7之间的事件处理方法(多于5个参数是个坏的实践,应该使用结构体或者EventArgs派生类来封装这些参数)。使用GenericEventHandler时可以定义任何类型的组合。例如:
GenericEventHandler<int> del1; GenericEventHandler<int,int> del2; GenericEventHandler<int,string> del3; GenericEventHandler<int,string,int> del4;
或者,为了传递多个参数:
struct MyStruct {...} public class MyArgs : EventArgs {...} GenericEventHandler<MyStruct> del5; GenericEventHandler<MyArgs> del6;
例子6-7展示了GenericEventHandler和泛型事件处理方法的用法。
public class MyArgs : EventArgs {...} public class MyPublisher { public event GenericEventHandler<MyPublisher,MyArgs> MyEvent; public void FireEvent( ) { MyArgs args = new MyArgs(...); MyEvent(this,args); } } public class MySubscriber<A> where A : EventArgs { public void OnEvent(MyPublisher sender,A args) {...} } MyPublisher publisher = new MyPublisher( ); MySubscriber<MyArgs> subscriber = new MySubscriber<MyArgs>( ); publisher.MyEvent += subscriber.OnEvent;
这个例子使用了两个类型参数的GenericEventHandler,sender类型和容器参数类,与泛型、非泛型的EventHandler非常相似。然而,不像EventHandler,GenericEventHandler是类型安全的,因为它只接受MyPublisher类型的对象(而不仅仅是对象就可以)作为sender。明显地,你可以使用GenericEventHandler处理所有的事件签名,包括那些参数不是sender对象和EventArgs的签名。
为了示范,例子6-7使用了泛型订阅服务器,接收一个泛型类型参数用作事件参数容器。你也可以用特定类型来替代它,而不影响发布服务器的代码:
public class MySubscriber { public void OnEvent(MyPublisher sender,MyArgs args) {...} }
如果你想要强制使用一个EventArgs派生类作为参数,你可以在GenericEventHandler上加个约束:
public delegate void GenericEventHandler<T,U>(T t,U u) where U : EventArgs;
然而,为了更广泛地使用GenericEventHandler,最好将约束放在订阅类型上(或者订阅方法),就像例子6-7那样。
警告:有时候对特定类型使用别名十分有用。你可以通过using语句来做:
using MyHandler = GenericEventHandler<MyPublisher,MyArgs>; public class MyPublisher { public event MyHandler MyEvent; //Rest of MyPublisher }
注意,别名的作用范围是在该文件范围,所以你需要在项目中重复该别名,就像使用命名空间一样。
6.2.4.Publishing Events Defensively
在.NET中,如果一个委托的内部列表没有任何目标对象,它的值将被置为null。C#服务发布器应该在试图调用委托前始终检查该委托的值是否为空。如果没有客户端订阅该事件,委托的目标对象列表会为空,并且该委托的值将为null。当发布服务器试图访问一个null值委托时,将抛出一个异常。基于委托的事件的另一个问题在于异常。任何订阅服务器未处理的异常将会传播给发布服务器。一些订阅服务器可能会在处理事件时遇到异常,但没有处理它,这将导致发布服务器挂掉。因此,你应该始终在发布事件时使用try/catch块。例子6-8展示了这些观点。
Example 6-8. Defensive publishing
public class MyPublisher { public event EventHandler MyEvent; public void FireEvent( ) { try { if(MyEvent != null) MyEvent(this,EventArgs.Empty); } catch { //Handle exceptions } } }
然而,例子6-8中的代码因为一个订阅服务器抛出了异常而中止了事件发布。有时候你想要在一个订阅服务器抛出异常之后继续发布事件。为了达到这个目的,你需要手动地遍历完委托内部的列表,捕捉列表中委托抛出的任何异常。你可以通过使用一个每个委托都支持的特殊方法来获取内部列表,这个方法叫做GetInvocationList()。这个方法的定义如下:
public virtual Delegate[] GetInvocationList( );
GetInvocationList()返回一个可以遍历的委托集合,正如例子6-9.
Example 6-9. Continuous publishing in the face ofexceptions thrown by the subscribers
public class MyPublisher { public event EventHandler MyEvent; public void FireEvent( ) { if(MyEvent == null) { return; } Delegate[] delegates = MyEvent.GetInvocationList( ); foreach(Delegate del in delegates) { EventHandler sink = (EventHandler)del; try { sink(this,EventArgs.Empty); } catch{} } } }
6.2.4.1 TheEventsHelper class
例子6-9中的问题在于代码不能重用,每次当你想要隔离发布服务器与订阅服务器的错误时就要复制这段代码。写一个帮助类来发布任何委托,传递任何参数集合,捕捉潜在的异常是可能的。例子6-10展示展示了EventsHelper静态类,它提供了静态方法Fire()。这个方法能防御性地出发任何类型的事件。
Example 6-10. The EventsHelper class
public static class EventsHelper { public static void Fire(Delegate del,params object[] args) { if(del == null) { return; } Delegate[] delegates = del.GetInvocationList( ); foreach(Delegate sink in delegates) { try { sink.DynamicInvoke(args); } catch{} } } }
实现EventsHelper有两个关键元素。第一点时调用任何委托的能力。这通过使用DynamicInvoke()方法达到,每个委托都提供该方法。DynamicInvoke()通过传递一个参数集合来调用委托。定义如下:
public object DynamicInvoke(object[] args);
第二点是将开放数量的对象作为参数传给订阅服务器。这通过C#params参数修饰符做到,允许一系列对象作为参数。编译器将这一系列参数转换为一个object数组,并传递该数组。使用EventsHelper是优雅且直接的:简单地传递要调用的委托和相关参数。例如,MyEventHandler,定义如下:
public delegate void MyEventHandler(int number,string str);
发布代码如下:
public class MyPublisher { public event MyEventHandler MyEvent; public void FireEvent(int number, string str) { EventsHelper.Fire(MyEvent,number,str); } }
6.2.4.2 MakingEventsHelper type-safe
例子6-10展示的代码中的EventsHelper存在无类型安全的问题。Fire()方法有一个无组织对象的集合,它允许任何组合形式的参数,包括不正确的组合。例如,给出一个委托定义:
public delegate void MyEventHandler(int number,string str);
下面的发布代码能进行编译但会在发布的时候失败:
public class MyPublisher { public event MyEventHandler MyEvent; public void FireEvent(int number, string str) { EventsHelper.Fire(MyEvent,"Not","Type","Safe"); } }
在编译时不会检测到参数数量或类型的不匹配。此外,EventsHelper会扼杀异常,也许你将不会注意到这个问题。
然而,如果你致力于使用GenericEventHandler替代定义自己的事件处理委托,有一种方法能强制编译时期类型安全。替代定义下面这个委托:
public delegate void MyEventHandler(int number,string str);
直接使用GenericEventHandler,或者给它别名:
using MyEventHandler = GenericEventHandler<int,string>;
接下来,结合EventsHelper和GenericEventHandler,正如例子6-11.
Example 6-11. The type-safe EventsHelper
public static class EventsHelper { //Same as Fire( ) in Example 6-10 public static void UnsafeFire(Delegate del,params object[] args) {...} public static void Fire(GenericEventHandler del) { UnsafeFire(del); } public static void Fire<T>(GenericEventHandler<T> del,T t) { UnsafeFire(del,t); } public static void Fire<T,U>(GenericEventHandler<T,U> del,T t,U u) { UnsafeFire(del,t,u); } public static void Fire<T,U,V>(GenericEventHandler<T,U,V> del,T t,U u,V v) { UnsafeFire(del,t,u,v); } public static void Fire<T,U,V,W>(GenericEventHandler<T,U,V,W> del,T t,U u,V v,W w) { UnsafeFire(del,t,u,v,w); } public static void Fire<T,U,V,W,X>(GenericEventHandler<T,U,V,W,X> del,T t,U u,V v,W w,X x) { UnsafeFire(del,t,u,v,w,x); } public static void Fire<T,U,V,W,X,Y>(GenericEventHandler<T,U,V,W,X,Y> del,T t,U u,V v,W w,X x,Y y) { UnsafeFire(del,t,u,v,w,x,y); } public static void Fire<T,U,V,W,X,Y,Z>(GenericEventHandler<T,U,V,W,X,Y,Z> del,T t,U u,V v,W w,X x,Y y,Z z) { UnsafeFire(del,t,u,v,w,x,y,z); } }
因为传递给GenericEventHandler的参数数量与类型是已知的,所以可以在编译时保证类型安全。感谢通过泛型类型参数数量的重载,你甚至不用指定传递给Fire()方法的泛型类型参数。发布事件的代码和使用例子6-10的代码相同:
using MyEventHandler = GenericEventHandler<int,string>; public class MyPublisher { public event MyEventHandler MyEvent; public void FireEvent(int number, string str) { //This is now type-safe EventsHelper.Fire(MyEvent,number,str); } }
编译器推断出使用的类型参数的类型和数量,然后选择正确的Fire()重载版本。
如果你确保只使用EventHandler而不是GenericEventHandler,你可以在EventsHelper中添加这些重载方法:
public static void Fire(EventHandler del,object sender,EventArgs e) { UnsafeFire(del,sender,e); } public static void Fire<E>(EventHandler<E> del,object sender,E e) where E : EventArgs { UnsafeFire(del,sender,t); }
这能使你防御性地发布事件,用一种类型安全的习惯,任何基于EventHandler的事件。
EventsHelper同样能提供非类型安全的UnsafeFire()方法:
public static void UnsafeFire(Delegate del,params object[] args) { if(args.Length > 7) { Trace.TraceWarning("Too many parameters. Consider a structure to enable the use of the type-safe versions"); } //Rest same as in Example 6-11 }
这在处理不是基于GenericEventHandler或者EventHandler得委托时要用到,或者当你要处理比GenericEventHandler参数数量多的委托时。显然,应避免定义使用多于5个参数的方法,但至少使用EventsHelper的开发者注意到了类型安全陷阱。如果你想始终强制使用类型安全的Fire()方法,只需将UnsafeFire()改为私有即可:
static void UnsafeFire(Delegate del,params object[] args);
6.2.5. Event Accessors
为了连接订阅服务器与发布服务器,你可以直接访问发布服务器的事件成员变量。将类成员暴露为公有是在寻找麻烦;它违背了面向对象设计的核心原则——封装和信息隐藏,并耦合了所有订阅服务器和确定的成员变量定义。为了减轻这个问题,C#提供了类似属性机制的事件访问器。访问器提供的好处和属性相似,隐藏实际类成员却维持了原本的易用性。C#使用add和remove执行+=和-=操作符,独自封装了事件成员变量。例子6-12演示了事件访问器的用法和相应的客户端代码。注意,当你使用事件访问器时,将已被封装的委托成员当做事件是没有好处的。
Example 6-12. Using event accessors
using MyEventHandler = GenericEventHandler<string>; public class MyPublisher { MyEventHandler m_MyEvent; public event MyEventHandler MyEvent { add { m_MyEvent += value; } remove { m_MyEvent -= value; } } public void FireEvent( ) { EventsHelper.Fire(m_MyEvent,"Hello"); } } public class MySubscriber { public void OnEvent(string message) { MessageBox.Show(message); } } //Client code: MyPublisher publisher = new MyPublisher( ); MySubscriber subscriber = new MySubscriber( ); //Set up connection: publisher.MyEvent += subscriber.OnEvent; publisher.FireEvent( ); //Tear down connection: publisher.MyEvent -= subscriber.OnEvent;
6.2.6. Managing Large Numbers of Events
想象一个发布了大量事件的类。这在开发框架时很常见;例如,在System.Windows.Forms命名空间中的Control类有非常多对于Windows消息的事件。处理大量事件的问题是为每个事件分配一个类成员是不可能的:类的定义,文档,实例工具视图,甚至智能感应都难以管理。为了解决该问题,.NET提供了EventHandlerList类(在System.ComponentModel命名空间):
public sealed class EventHandlerList : IDisposable { public EventHandlerList( ); public Delegate this[object key]{get;set;} public void AddHandler(object key, Delegate value); public void AddHandlers(EventHandlerList listToAddFrom); public void RemoveHandler(object key, Delegate value); public virtual void Dispose( ); }
EventHandlerList是一个存储键值对的线性列表。键是一个标识事件的对象,值是一个System.Delegate实例。因为索引是一个对象,它可以是一个整数、字符串、一个具体的按钮实例等等。通过使用AddHandler和RemoveHandler方法来添加和移除一个事件处理方法。你也可以使用AddHandlers()方法直接添加一个现有的EventHandlerList。要触发一个事件,需要通过键对象来索引得到事件列表,会得到一个System.Delegate对象。接下来需要将这个委托转换为实际的事件委托并触发事件。
例子6-13展示了如何使用EventHandlerList类,在实现一个类似Windows Forms按钮叫做MyButton的类。这个按钮支持很多事件,例如鼠标点击和鼠标移动,它们都通向同一个事件列表。使用事件访问器,将完全封装信息。
Example 6-13. Using the EventHandlerList class to manage a large number of events
using System.ComponentModel; using ClickEventHandler = GenericEventHandler<MyButton,EventArgs>; using MouseEventHandler = GenericEventHandler<MyButton,MouseEventArgs>; public class MyButton { EventHandlerList m_EventList; public MyButton( ) { m_EventList = new EventHandlerList( ); /* Rest of the initialization */ } public event ClickEventHandler Click { add { m_EventList.AddHandler("Click",value); } remove { m_EventList.RemoveHandler("Click",value); } } public event MouseEventHandler MouseMove { add { m_EventList.AddHandler("MouseMove",value); } remove { m_EventList.RemoveHandler("MouseMove",value); } } void FireClick( ) { ClickEventHandler handler = m_EventList["Click"] as ClickEventHandler; EventsHelper.Fire(handler,this,EventArgs.Empty); } void FireMouseMove(MouseButtons button,int clicks,int x,int y,int delta) { MouseEventHandler handler = m_EventList["MouseMove"] as MouseEventHandler; MouseEventArgs args = new MouseEventArgs(button,clicks,x,y,delta); EventsHelper.Fire(handler,this,args); } /* Other methods and events definition */ }
例子6-13的问题是例如鼠标移动、鼠标点击等事件触发很频繁,每次调用新建一个字符串当做键会增加托管堆的压力。一个更好的方法是使用预分配的静态键,在所有实例中共享:
public class MyButton { EventHandlerList m_EventList; static object m_MouseMoveEventKey = new object( ); public event MouseEventHandler MouseMove { add { m_EventList.AddHandler(m_MouseMoveEventKey,value); } remove { m_EventList.RemoveHandler(m_MouseMoveEventKey,value); } } void FireMouseMove(MouseButtons button,int clicks,int x,int y,int delta) { MouseEventHandler handler; handler = m_EventList[m_MouseMoveEventKey] as MouseEventHandler; MouseEventArgs args = new MouseEventArgs(button,clicks,x,y,delta); EventsHelper.Fire(handler,this,args); } /* Rest of the implementation */ }
6.2.7. Writing Sink Interfaces
通过隐藏实际的事件成员,事件访问器提供勉强的封装性。然而,你可以增强这个模型。为了说明,考虑下面情况,一个订阅服务器希望订阅一系列事件。为什么它要进行多次可能开销昂贵的调用去设置和断开连接?为什么订阅服务器首先需要知道事件访问器?如果订阅服务器想要用接口代替方法来接收事件会怎么样?接下来的要提供一个简单却广泛的方式来管理发布服务器与订阅服务器之间的连接,这样能节约不必要的调用,封装事件访问器和成员,并允许订阅接口。本部分会描述我开发的用来实现这些的技术。考虑定义了一系列事件的一个接口,ImySubscriber接口:
public interface IMySubscriber { void OnEvent1(object sender,EventArgs eventArgs); void OnEvent2(object sender,EventArgs eventArgs); void OnEvent3(object sender,EventArgs eventArgs); }
任何人可以实现这个接口,并且所有的发布服务器应该知道它:
public class MySubscriber : IMySubscriber { public void OnEvent1(object sender,EventArgs eventArgs) {...} public void OnEvent2(object sender,EventArgs eventArgs) {...} public void OnEvent3(object sender,EventArgs eventArgs) {...} }
接下来,定义一个关于此事件的枚举,并用Flags特性标记:
[Flags] public enum EventType { OnEvent1, OnEvent2, OnEvent3, OnAllEvents = OnEvent1|OnEvent2|OnEvent3 }
Flags特性指出这个枚举的值可以用作位屏蔽(例如EventType.OnAllEvents的定义)。这允许你使用|按位操作符(按位或)来结合不同的枚举值或者使用&操作符(按位与)来屏蔽他们。
发布服务器提供两个方法,Subscribe()和Unsubscribe(),都接受两个参数:接口和一个位掩码标志指出接收服务器接口订阅的是哪个事件。发布服务器的每个方法可以在接收服务器内拥有一个相对应的事件委托成员,或者是一个对应所有方法的委托成员(这是被订阅服务器隐藏的实现细节)。例子6-14在接收服务器中为每个方法使用了一个事件成员变量。它展示了Subscribe()和Unsubscribe()和FireEvent()方法,为了清楚明了移除了错误处理代码。Subscribe()检查标志并订阅相应的接口方法:
if((eventType & EventType.OnEvent1) == EventType.OnEvent1) { m_Event1 += subscriber.OnEvent1; }
UnSubscribe()使用同样风格来移除订阅。
Example 6-14. Sinking interfaces
using MyEventHandler = GenericEventHandler<object,EventArgs>;
public class MyPublisher
{
MyEventHandler m_Event1;
MyEventHandler m_Event2;
MyEventHandler m_Event3;
public void Subscribe(IMySubscriber subscriber,EventType eventType)
{
if((eventType & EventType.OnEvent1) == EventType.OnEvent1) { m_Event1 += subscriber.OnEvent1; }
if((eventType & EventType.OnEvent2) == EventType.OnEvent2)
{
m_Event2 += subscriber.OnEvent2;
}
if((eventType & EventType.OnEvent3) == EventType.OnEvent3)
{
m_Event3 += subscriber.OnEvent3;
}
}
public void Unsubscribe(IMySubscriber subscriber,EventType eventType)
{
if((eventType & EventType.OnEvent1) == EventType.OnEvent1)
{
m_Event1 -= subscriber.OnEvent1;
}
if((eventType & EventType.OnEvent2) == EventType.OnEvent2)
{
m_Event2 -= subscriber.OnEvent2;
}
if((eventType & EventType.OnEvent3) == EventType.OnEvent3)
{
m_Event3 -= subscriber.OnEvent3;
}
}
public void FireEvent(EventType eventType)
{
if((eventType & EventType.OnEvent1) == EventType.OnEvent1)
{
EventsHelper.Fire(m_Event1,this,EventArgs.Empty);
}
if((eventType & EventType.OnEvent2) == EventType.OnEvent2)
{
EventsHelper.Fire(m_Event2,this,EventArgs.Empty);
}
if((eventType & EventType.OnEvent3) == EventType.OnEvent3)
{
EventsHelper.Fire(m_Event3,this,EventArgs.Empty);
}
}
}
订阅和取消订阅的代码都十分简单:
MyPublisher publisher = new MyPublisher( ); IMySubscriber subscriber = new MySubscriber( ); //Subscribe to events 1 and 2 publisher.Subscribe(subscriber,EventType.OnEvent1|EventType.OnEvent2); //Fire just event 1 publisher.FireEvent(EventType.OnEvent1);
不过,它展示了一种优雅的方式通过一次调用订阅所有接口,同样也展示了封装事件的类的复杂性。
相关文章推荐
- Programming .NET Components 2nd 学习笔记(三)
- Programming .NET Components 2nd 学习笔记(八)
- Programming .NET Components 2nd 学习笔记(一)
- Programming .NET Components 2nd 学习笔记(二)
- Programming .NET Components 2nd 学习笔记(十一)
- Programming .NET Components 2nd 学习笔记(十二)
- Programming ASP.NET 学习笔记(要点)第3章 控件:基本概念
- Programming .NET Components 2nd 学习笔记(七)
- Programming .NET Components 2nd 学习笔记(四)
- Programming .NET Components, 2nd Edition [ILLUSTRATED]
- Programming .NET Components 2nd 学习笔记(五)
- Programming .NET Components 2nd 学习笔记(六)
- Programming .NET Components 2nd 学习笔记(十)
- MathNet的学习笔记2
- ASP.NET MVC学习笔记:(一)路由匹配
- ASP.Net MVC开发基础学习笔记(8):新建数据页面
- ASP.NET MVC Web API 学习笔记---联系人增删改查 (转载)
- OSGi.NET 学习笔记 [目录]
- CUBRID学习笔记 32 对net的datatable的支持 cubrid教程
- Introduction to 3D Game Programming with DirectX 11学习笔记 1~3章