使用.NET事件工作
定义委托签名
虽然技术上来讲,一个委托声明可以定义为任意的方法签名,但在实际中,事件委托应当遵守一些特殊的建议。
首先,目标方法应当拥有一个void返回类型。原因是很简单,没有道理对事件发布者返回一个值。事件发布者不关心事件订阅者为什么要订阅事件。此外,委托类从发布者哪里隐藏了实际的发布动作。委托是用来迭代其内部订阅者对象列表,并调用每个适当的方法。因此,返回值不会传播到发布者代码中。同样的,也应该避免使用ref或out的output参数,因为它们的值也不会传播到发布者代码中。
第二,一些订阅者可能想要从多个事件发布者源上接收同一个事件。因为在提供多个发布者的多个方法上不存在灵活性,订阅者会想对于多个发布者提供同一个方法。为了允许订阅者在不同的发布者激发事件时能够进行识别,委托签名应当包含发布者标识。不依赖于泛型的话,最简单的方式就是新增一个object类型参数,称为sender参数,发布者直接把自身作为sender参数传入:
public delegate void NumberChangedEventHandler(object sender,int number);
最终,定义的实际事件参数(如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即可。如果事件需求参数,则可以继承EventArgs类,例如NumberChangedEventArgs,并增加成员变量,方法或者属性来满足需求,在激发事件时传入该继承类。订阅者通过特定的与事件关联的参数类(如NumberChangedEventArgs)来访问事件参数。继承EventArgs类并传入指定参数,这同时也就允许新增参数或移除不用的参数。当出现订阅者不关心的新信息时,订阅者不必强迫变化。
public delegate void NumberChangedEventHandler(object sender,EventArgs eventArgs);
public class NumberChangedEventArgs : EventArgs
{
public int Number; //应当作为属性
}
public class MyPublisher
{
public event NumberChangedEventHandler NumberChanged;
public void FireNewNumberEvent(EventArgs eventArgs)
{
//在调用之前总是检查委托是否为空
if (NumberChanged != null)
{
NumberChanged(this, eventArgs);
}
}
}
public class MySubcriber
{
public void OnNumberChanged(object sender, EventArgs eventArgs)
{
NumberChangedEventArgs numberArg;
numberArg = eventArgs as NumberChangedEventArgs;
Debug.Assert(numberArg != null);
MessageBox.Show("The new number is " + numberArg.Number);
}
}
//客户端代码
MyPublisher publisher = new MyPublisher();
MySubcriber subcriber = new MySubcriber();
publisher.NumberChanged += subcriber.OnNumberChanged;
NumberChangedEventArgs numberArg = new NumberChangedEventArgs();
numberArg.Number = 4;
//注意,发布者不需要知道参数的类型
publisher.FireNewNumberEvent(numberArg);
因为现在委托定义的结果是可适配的,.NET提供了EventHandler委托:
public delegate void EventHandler(object sender,EventArgs eventArgs);
EventHandler可以通过.NET应用程序框架广泛地被使用,例如Window Forms和ASP.NET。但是,一个无定形的基类的灵活性会带来类型安全上的损失。为了解决这个问题,.NET提供了一个EventHandler的泛型版本:
public delegate void EventHandler<E>(object sender,E e) where E:EventArgs;
该订阅就会认为所以的事件处理方法都是object sender和继承于EventArgs类的格式存在,这个委托可以满足所以的需求。在所以其他情况下,仍旧可以为需要处理的特殊事件处理签名来定义自己的委托。
定义自定义事件参数
正如前面所述,应当给事件处理以EventArgs继承类的形式提供参数,参数作为该继承类的成员出现。委托迭代其订阅者列表,依次向订阅者传入参数对象。但是,这样就没有任何措施来防止某一订阅者修改参数值而影响到其他处理该事件的订阅者。通常,应当防止这种情况的出现。为了阻止改变参数值,可以提供只读访问的参数或者以公共成员的方式来暴露参数并应用readonly修饰符。在以上两种做法中,都要在构造器中初始化参数值:
public class NumberEventArg1 : EventArgs
{
public readonly int Number;
public NumberEventArg1(int number)
{
Number = number;
}
}
public class NumberEventArg2 : EventArgs
{
int _number;
public int Number
{
get { return _number; }
}
public NumberEventArg2(int number)
{
_number = number;
}
}
泛型事件处理
当泛型委托来源于事件的时候,它会显得特别有用。优秀的用于管理事件的委托,应当是返回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版本可以用于调用任何接收零到七个参数的事件处理方法(多于五个参数是种不好的做法,应当使用结构,或在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;
下面这个示例展示了GenericEventHandler和一个泛型事件处理方法的用法:
public class MyPublisher
{
public event GenericEventHandler<MyPublisher, MyArgs> MyEvent;
public void FireEvent()
{
MyArgs args = new MyArgs(…);
if (MyEvent != null)
{
MyEvent(this, args);
}
}
}
public class MySubcriber<A> where A: EventArgs
{
public void OnEvent(MyPublisher sender, A args)
{… }
}
MyPublisher publisher = new MyPublisher();
MySubcriber<MyArgs> subcriber = new MySubcriber<MyArgs>();
publisher.MyEvent += subcriber.OnEvent;
该示例中使用带有两个类型参数的GenericEventHandler,sender类型和参数容器类。与EventHandler不同的是,GenericEventHandler是类型安全的。因为它只接受类型为MyPublisher的对象作为sender。可以在所以事件签名上使用GenericEventHandler,包括没有遵守sender对象和EventArgs继承类原则的那些委托。
为了明确目的,上面的示例中同时使用了一个泛型订阅者,它只接受对于事件参数容器的泛型类型参数。当然也可以使用特定的参数容器类型来定义订阅者,而不会影响到发布者代码:
public class MySubcriber
{
public void OnEvent(MyPublisher sender, MyArgs args)
{.. }
}
如果想要强制使用一个EventArgs的继承类作为参数,可以在GenericEventHandler上放置一个约束。但是,为了GenericEventHandler的广泛使用,最好在订阅者类型上放置约束:
public delegate void GenericEventHandler<T, U>(T t, U u) where U:EventArgs;
防御式事件发布
在.NET中,如果一个委托的内部列表中没有任何目标方法,它的值会被设为null。C#发布者应当总是在试图调用委托之前检查委托是否为null,否则就会抛出异常。另一个基于委托的事件机制存在的问题是异常。任何不可处理的异常都会通过订阅者而传播到发布者。一些订阅者可能在其事件处理中会遭遇异常而没有处理它,这会导致发布者损坏。为了解决这些问题,应当总是在try/catch代码块中发布事件:
public class MyPublisher
{
public event EventHandler MyEvent;
public void FireEvent()
{
try
{
if (MyEvent != null)
MyEvent(this, EventArgs.Empty);
}
catch
{
//处理异常
}
}
}
但是,退出事件发布会导致订阅者抛出一个异常。有时想要即使一个订阅者抛出异常,而事件发布继续进行。这样就需要通过委托手动迭代内部列表维护,并通过列表中另一个单独的委托来捕获所有异常。可以通过使用一个每个委托都支持的特殊方法来访问内部列表,称为GetInvocationList(),返回一个可迭代的委托列表:
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)
try
{
sink(this, EventArgs.Empty);
}
catch { }
}
}
}
一、事件辅助类
在上面示例中的事件发布代码存在的问题是不可重用——不得不在每次想要在发布者和订阅者之间进行错误隔离时复制代码。因此,应编写一个可以发布事件到任何委托,可以传入任何参数集合,并可以捕获潜在异常的辅助类。下面展示的就是EventsHelper静态类,该类提供了Fire()静态方法,该方法可以防御式激发任何事件类型:
public static class EventsHelper
{
public static void Fire(Delegate del, params object[] args)
{
if (
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修饰符来处理,这样就允许使用对象的递归作为参数。编译器会转换该递归参数到一个对象数组中,并传入该数组。使用EventsHelper既优雅又明确:直接传入委托和参数就可以了。下面是使用示例:
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);
}
}
二、使EventsHelper类型安全
现在EventsHelper面临的问题是非类型安全。Fire()方法使用object对象集合,这就是允传入许任何参数组合,包括不正确的组合。例如以下委托定义,在发布者代码中会通过编译,当在运行时发布事件就会出错:
public class MyPublisher
{
public event MyEventHandler MyEvent;
public void FireEvent(int number, string str)
{
EventsHelper.Fire(MyEvent, "Not", "Type", "Safe");
}
}
任何参数数目的不匹配或参数的类型都不会在编译时检测。此外,EventsHelper会扼杀异常,甚至不会知道出现了问题。但是,如果可以总是使用GenericEventHandler来代替自定义事件处理委托,就可以强制实施编译时类型安全。直接使用GenericEventHandler或重命名它:
//原定义
//public delegate void MyEventHandler(int number, string str);
//使用GenericEventHandler定义
using MyEventHandler = GenericEventHandler<int, string>;
下面把GenericEventHandler整合到EventsHelper中:
public static class EventsHelper
{
public static void UnsafeFire(Delegate del, params object[] args)
{
if (
return;
Delegate[] delegates = del.GetInvocationList();
foreach (Delegate sink in delegates)
{
try
{
sink.DynamicInvoke(args);
}
catch
{ }
}
}
public static void Fire(GenericEventHandler del)
{
UnsafeFire(
}
public static void Fire<T>(GenericEventHandler<T>
{
UnsafeFire(
}
public static void Fire<T, U>(GenericEventHandler<T, U>
{
UnsafeFire(
}
//代码省略
public static void Fire<T, U, V, W, X, Y, Z>(GenericEventHandler<T, U, V, W, X, Y, Z>
{
UnsafeFire(
}
}
因为传入GenericEventHandler参数的数目和类型对于编译器是已知的,这样就可以做到编译时的类型安全。得益于泛型类型参数数目的重载,不再需要指定任何泛型类型参数传到Fire()方法,发布者代码不需做任何修改就做到了类型安全。编译器会推断使用的类型参数数目和类型,并选择正确的Fire()重载版本:
public class MyPublisher
{
public event MyEventHandler MyEvent;
public void FireEvent(int number, string str)
{
//现在是类型安全的
EventsHelper.Fire(MyEvent, number, str);
}
}
如果只保证EventHandler的使用。那么可以新增以下这些重载方法到EventsHelper中,同样会确保所有基于EventHandler的事件发布是类型安全的:
public static void Fire(EventHandler del, object sender, EventArgs e)
{
UnsafeFire(
}
public static void Fire<E>(EventHandler<E>
{
UnsafeFire(
}
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");
}
if (
return;
Delegate[] delegates = del.GetInvocationList();
foreach (Delegate sink in delegates)
{
try
{
sink.DynamicInvoke(args);
}
catch
{ }
}
}
这个要求是在处理不是基于GenericEventHandler或EventHandler的委托,或是参数类型数目多于GenericEventHandler可处理的数目时而存在的。如果总是要强制类型安全的Fire()方法的使用,请把UnsafeFire()方法定义为私有方法。
根据原版英文翻译,所以不足和错误之处请大家不吝指正,谢谢:)