Effective C# 原则35:选择重写函数而不是使用事件句柄
很多.Net类提供了两种不同的方法来控制一些系统的事件。那就是,要么添加一个事件句柄;要么重写基类的虚函数。为什么要提供两个方法来完成同样的事情呢?其实很简单,那就是因为不同的情况下要调用为的方法。在派生类的内部,你应该总是重写虚函数。而对于你的用户,则应该限制他们只使用句柄来响应一些不相关的对象上的事件。
例如你很了一个很不错的Windows应用程序,它要响应鼠标点下的事件。在你的窗体类中,你可以选择重写OnMouseDown()方法:
public class MyForm : Form
{
// Other code elided.
protected override void OnMouseDown(MouseEventArgs e)
{
try {
HandleMouseDown( e );
} catch ( Exception e1 )
{
// add specific error handling here.
}
// *almost always* call base class to let
// other event handlers process message.
// Users of your class expect it.
base.OnMouseDown( e );
}
}
或者你可以添加一个事件句柄:
public class MyForm : Form
{
// Other code elided.
public MyForm( )
{
this.MouseDown += new MouseEventHandler(this.MouseDownHandler);
}
private void MouseDownHandler(object sender,MouseEventArgs e)
{
try {
HandleMouseDown( e );
} catch ( Exception e1 )
{
// add specific error handling here.
}
}
}
前面一些方法要好一些,如果在事件链上有一个句柄抛出了一个异常,那么其它的句柄都不会再被调用(参见原则21)。一些“病态”的代码会阻止系统调用事件上的句柄。通过重写受保护的虚函数,你的控制句柄会就先执行。基类上的虚函数有责任调用详细事件上的所有添加的句柄。这就是说,如果你希望事件上的句柄被调用(而且这是你最想完成的),你就必须调用基类。而在一些罕见的类中,你希望取代基类中的默认事件行为,这样可以让事件上的句柄都不被执行。你不去保证所所的事件句柄都将被调用,那是因为一些“病态”事件句柄可能会引发一些异常,但你可以保证你派生类的行为是正确的。
使用重载比添加事件句柄更高效。我已经在原则22中告诉过你,System.Windows.Forms.Control类是如何世故的使用任命机制来存储事件句柄,然后映射恰当的句柄到详细的事件上。这种事件机制要花上更多的处理器时间,那是因为它必须检测事件,看它是否有事件句柄添加在上面。如果有,它就必须迭代整个调用链表。方法链表中的每个方法都必须调用。断定有哪些事件句柄在那里,还要对它们进行运行时迭代,这与只调用一个虚函数来说,要花上更多的执行时间。
如果这还不足以让你决定使用重载,那就再看看这一原则一开始的链表。那一个更清楚?如果重载虚函数,当你在维护这个窗体时,只有一个函数要检查和修改。而事件机制则有两个地方要维护:一个就是事件句柄,另一就是事件句柄上的函数。任何一个都可能出现失败。就一个函数更简单一些。
OK,我已经给出了所有要求使用重载而不是事件句柄的原因。.Net框架的设计者必须要添加事件给某人,对吗?当然是这样的。就你我们剩下的内容一个,他们太忙了而没时间写一些没人使用的代码。重写只是为派生类提供的,其它类必须使用事件机制。例如,你经常添加一个按钮点击事件到一个窗体上。事件是由按钮触发的,但是由窗体对象处理着事件。你完全可以在这个类中定义一个用户的按钮,而且重写这个点击句柄,但这对于只是处理一个事件来说花上了太多的代码。不管怎样,问题都是交给你自己的类了:你自己定义的按钮还是在点击时必须与窗体进行通信。显然应该用事件来处理。因此,最后,你只不过是创建了一个新类来向窗体发送事件(译注:其实我们完全可以创建这个类不用发事件给窗体就可以完成回调的,只是作者习惯的说什么好就一味的否定其它。但不管怎样,重写一个按钮来重载函数确实不是很值。)。相对前面一种方法,直接在窗体事件添加句柄要简单得多。这也就是为什么.Net框架的设计者把事件放在窗体的最前面。
另一个要使用事件的原因就是,事件是在运行时处理的。使用事件有更大的伸缩性。你可以在一个事件上添加多个句柄,这取决于程序的实际环境。假设你写了一个绘图程序,根据程序的状态,鼠标点下时应该画一条线,或者这它是要选择一个对象。当用户切换功能模式时,你可以切换事件句柄。不同的类,有着不同的事件句柄,而处理的事件则取决于应用程序的状态。
最后,对于事件,你可以把多个事件句柄挂到同样的事件上。还是想象同样的绘图程序,你可能在MouseDown事件上挂接了多个事件句柄。第一个可能是完成详细的功能,第二个可能是更新状态条或者更新一些可访问的不同命令。不同的行为可以在同一事件上响应。
当你有一个派生类中只有一个函数处理一个事件时,重载是最好的方法。这更容易维护,今后也会更正确,而且更高效。而应该为其它用户保留事件。因此,我们应该选择重写基类的实现而不是添加事件句柄。