简介
在前述的博文中我们总是提到事件和委托,而Windows编程决离不开”事件”,现在当使用.Net编程时”委托”又是必不可少的。在这里我们对事件和委托做一个总结,向您呈现到底什么是.Net事件和委托、两者的关系以及如何熟练使用它们。
什么是委托 delegate
.Net下事件和委托总是一个整体,必须一起来阐述。在前面博文中我们已经或多或少的介绍过,其实委托就是一个函数指针,一个委托型变量就是在维护一个函数列表,保存函数的地址或者称为引用。
delegate 实际是一个类,当创建一个委托时,需要向delegate的构造函数传递一个函数名。
任何委托都必须有特定的签名,例如定义一个委托类型SomeDelegate:
delegate int SomeDelegate(string s, bool, b);
当我们说委托、函数或者方法具有什么样的签名时,就是指这个委托、函数或者方法需要传递什么类型的参数以及返回值的类型等等。当初始化一个委托时你必须传递一个实际的函数。这时,一个非常重要的事情需要注意:只有与委托具备完全一致的签名的函数才能进行传递,例如名为SomeFunction的函数:
private int SomeFunction(string str, bool bln){...}
这个函数是可以传递给前述SomeDelegate委托的,因为他们具备相同的签名!
SomeDelegate sd = new SomeDelegate(SomeFunction);
好,现在名为sd的SomeDelegate类型的变量已经指向SomeFunction,或者说SomeFunction已经注册到sd!当调用sd时,SomeFunction将会被调用。注意,为什么说SomeFunction注册到sd呢?稍后,我们将涉及到这个问题。
sd("somestring", true);
理解事件 event
Button是一个类,当你单击它的时候,click事件被激活。 Timer也是一个类,每次毫秒逝去都会激活tick事件。 ……
让我们通过一个经典的计数示例程序来理解这其中的奥秘。我们有个Counter类,这个类有有个方法名为CountTo: public void CountTo(int countTo, int reachableNum) 它从0开始计数到自定义整数变量countTo,当到达reachableNum时将触发名为NumberReached的事件。
事件其实就是委托类型的变量,也就是说如果想声明一个事件的话,你必须先声明一个委托类型的变量,然后将event关键字放在声明体的前部,例如:
public event NumberReachedEventHandler NumberReached;
在上面的声明中NumberReachedEventHandler就是一个委托。既然是个委托也许我们应该写为NumberReachedDelegate,但是我们注意到微软并不以MouseDelegate或者PaintDelegate等格式来命名事件,而是以MouseEventHandler和PaintEventHandler的形式来命名,所以我们也按照惯例来把NumberReachedDelegate命名为NumberReachedEventHandler。
所以,当我们声明一个事件时必须提前定义好一个委托,例如:
public delegate void NumberReachedEventHandler(object sender, NumberReachedEventArgs e);
NumberReachedEventHandler具有object和NumberReachedEventArgs类型的传递参数以及void返回值的签名,当实例化这个委托时,传递的函数签名必须与该委托的签名保持一致。
现在来看一下NumberReachedEventArgs,设想当你想确定鼠标移动到的位置以及想存取Paint事件的Graphics属性时都会分别用到MouseEventArgs和PaintEventArgs,实际上这两个类都是继承自EventArgs类,来向用户传递事件发生时的相关数据。在我们的示例中,我们会传递那个到达的数即reachableNum:
public class NumberReachedEventArgs : EventArgs
{
private int _reached;
public NumberReachedEventArgs(int num)
{
this._reached = num;
}
public int ReachedNumber
{
get
{
return _reached;
}
}
}
如果在事件发生时没必要传递数据的话,我们可以直接使用EventArgs,而不必再自定义。
好,现在来看看我们的Counter类:
namespace Events
{
public delegate void NumberReachedEventHandler(object sender,
NumberReachedEventArgs e);
/// <summary>
/// Summary description for Counter.
/// </summary>
public class Counter
{
public event NumberReachedEventHandler NumberReached;
public Counter()
{
//
// TODO: Add constructor logic here
//
}
public void CountTo(int countTo, int reachableNum)
{
if(countTo < reachableNum)
throw new ArgumentException(
"reachableNum should be less than countTo");
for(int ctr=0;ctr<=countTo;ctr++)
{
if(ctr == reachableNum)
{
NumberReachedEventArgs e = new NumberReachedEventArgs(
reachableNum);
OnNumberReached(e);
return;//don't count any more
}
}
}
protected virtual void OnNumberReached(NumberReachedEventArgs e)
{
if(NumberReached != null)
{
NumberReached(this, e);//Raise the event
}
}
}
}
在上述的代码中,当到达指定的数值时事件就会被触发。这里有许多事情需要考虑: 通过调用NumberReached(即NumberReachedEventHandler委托类型的实例)来触发事件;
NumberReached(this, e);
这时所有已经注册的函数都将被调用执行。
我们定义事件中需要传递的数据:
NumberReachedEventArgs e = new NumberReachedEventArgs(reachableNum);
也许你会问:为什么我们不直接调用NumberReached(this, e),而是间接的调用OnNumberReached(NumberReachedEventArgs e)方法呢?为什么我们不能写成下面的形式:
if(ctr == reachableNum)
{
NumberReachedEventArgs e = new NumberReachedEventArgs(reachableNum);
//OnNumberReached(e);
if(NumberReached != null)
{
NumberReached(this, e);//Raise the event
}
return;//don't count any more
}
问得好!我们先看下OnNumberReached的签名:
protected virtual void OnNumberReached(NumberReachedEventArgs e)
这个方法是protected的,那么在所有的继承类或者说子类中都是可以访问它的 这个方法是虚拟的,也就是说它可以在任何继承类或者说子类中被重载
这样做是非常有用处的!假如你又设计了一个继承自Counter的类,通过重载OnNumberReached方法,你可以在事件被激活前做一些附加的操作,例如:
protected override void OnNumberReached(NumberReachedEventArgs e)
{
//Do additional work
base.OnNumberReached(e);
}
** 注意,如果没有调用基类的base.OnNumberReached(e),那么事件将永远不会在这个继承类中被触发!这样做的好处就是你可以在继承类中屏蔽不需要的事件!!**
** 注意,NumberReachedEventHandler委托定义在我们的Counter类之外,但却是在同一命名空间下,所以这个委托对该命名空间下的所有类都是可见的。**
好了,现在该是使用我们创建的Counter类的时候了。在示例程序中,我们有两个名为txtCountTo和txtReachable的textbox控件,界面如下:
下面是btnRun的单击事件处理:
private void cmdRun_Click(object sender, System.EventArgs e)
{
if(txtCountTo.Text == "" || txtReachable.Text=="")
return;
oCounter = new Counter();
oCounter.NumberReached += new NumberReachedEventHandler(
oCounter_NumberReached);
oCounter.CountTo(Convert.ToInt32(txtCountTo.Text),
Convert.ToInt32(txtReachable.Text));
}
private void oCounter_NumberReached(object sender, NumberReachedEventArgs e)
{
MessageBox.Show("Reached: " + e.ReachedNumber.ToString());
}
初始化一个事件处理的语法如下:
oCounter.NumberReached += new NumberReachedEventHandler(
oCounter_NumberReached);
现在,你该明白是怎么回事了吧?没错,我们刚刚实例化了NumberReachedEventHandler类型的实例。请注意oCounter_NumberReached方法的签名和我们前述的委托签名非常相似。而且再请注意,我们使用了+=来代替=!这就是为什么委托是一种特殊的类,并且可以维护超过一个对象的引用,在本文中委托维护一个函数调用列表。例如,如果你还有另外的方法名为oCounter_NumberReached2,并且与oCounter_NumberReached具有同样的函数签名,那么可以通过这种写法将所有方法都注册到这个委托上来:
oCounter.NumberReached += new NumberReachedEventHandler(
oCounter_NumberReached);
oCounter.NumberReached += new NumberReachedEventHandler(
oCounter_NumberReached2);
当激活这个事件后,所有已注册的方法都将依次被执行!如果不打算在事件发生时还执行oCounter_NumberReached2的话,你可以这样取消这个方法:
oCounter.NumberReached -= new NumberReachedEventHandler(
oCounter_NumberReached2);
event 关键字
有很多人都会问:如果我在委托前不使用event关键将会发生什么呢?
本质上来讲,声明event可以阻止委托被赋值为null!这真的很重要吗?当然!设想,如果一个委托前面没有注明event,向委托注册一个函数时肯定是需要用到”+=”,但是如果写成”=”的话就会对委托重新赋值,这时委托已经被一个全新的实例所取代,并且丢弃了已经注册到委托的所有函数,所有需要执行的函数或方法都无法被执行。致命的是,系统也不会因此报告任何编译错误,甚至是运行时错误!面对这种情形,我们就需要event关键字来进行解决。当我们把 public event NumberReachedEventHandler NumberReached 前的event关键字去除,我们可以这样写:
oCounter.NumberReached = new NumberReachedEventHandler(oCounter_NumberReached);
这样写编译器决不会报错,但是却不是我们想要的!如果保留event关键字,上述赋值语句在编译时就会报错!提示必须使用+=或者-+,例如:
总之,event关键字是委托实例的一个保护层,它可以阻止对委托进行清空操作,而只是允许向委托调用列表添加和移除对象。