事件和委托的简要概述
前言
当我尝试去学习Events和Delegates的时候,我看了很多的文章以便能够完全的理解他们是什么,以及如何使用它们,现在我要把我学习到的知识点和你们分享,大部分都是作为初学者必须会的。
什么是委托
委托和事件的概念是绑定在一起的。委托就是函数指针,也就是说它们是指向函数的“引用”。
委托本质上是一个类。当你创建这个类的实例的时候,你把函数的名字作为一个参数传递给这个委托的构造函数,这个委托就能引用这个函数了。
每一个委托都有标记(signature),例如:
Delegate int SomeDelegate(string s, bool b);
上面就是一个委托的声明。当我说这个委托的有标记的时候,我的意思是它的返回值是int
类型的值,传入参数是两个类型分别为string
和bool
的值。
当你实例化委托,你把这个委托要引用的函数的函数名作为参数传递到委托的构造函数。需要注意的是,只有当该函数和这个委托的标记一样的时候才能够作为参数传递进去。
考虑下面的函数
private int SomeFunction(string str, bool bln){...}
可以把这个函数传递到SomeDelegate
的构造函数中。
SomeDelegate sd = new SomeDelegate(SomeFunction);
现在sd
引用了SomeFunction
,换句话说,SomeFunction
注册到了sd下。如果你调用sd,实际上运行的是SomeFunction
。记住我所说的被注册的函数的含义,随后我们将会涉及到它们。
sd("somestring", true);
现在你已经知道了如何使用委托,让我们再去理解什么是事件。
什么是事件
Button
是一个类,当你点击它的时候,click
时间触发。Timer
是一个类,每一个毫秒都会触发tick
事件。
想知道发生了什么?让我们通过一个例子来说明:
情形是这样的:我们有个叫
Counter
的类。这个类有个方法CountTo(int countTo, int reacheableNum)
从0开始计时到countTo
,当这个数达到reacheableNum
的时候就会触发一个称为NumberReached
的事件。
我们设想的这个类中有个事件:NumberReached
。事件就是委托类型的变量。如果你想要声明一个时间,你其实就是声明一个变量,这个变量是某种委托,只不过在这个委托的前面加了关键字:event
,就像这样:
public event NumberReachedEventHandler NumberReached;
在上述的声明中,NumberReachedEventHandler
就是一个委托。或许更确切的应该命名为:NumberReachedDelegate
,但是需要注意的是,微软不会说MouseDelegate
或者PaintDelegate
,而是提供了MouseEventHandler
和PaintEventHandler
。这只是为了方便起见用NumberReachedEventHandler
代替NumberReachedDelegate
。
你看,在我们声明我们的事件之前,需要定义委托(也就是事件的处理函数(event handler)),就像下面这样:
public delegate void NumberReachedEventHandler(object sender, NumberReachedEventArgs e);
如上所述,我们的委托叫NumberReachedEventHandler
,它的标记包含了void
的返回值,object
和NumberReachedEventArgs
类型的传入参数。作为参数传递到这个委托的构造函数的函数,需要有和这个标记相同的参数。
你是否在你的代码中用过PaintEventArgs
或者MouseEventArgs
,用于在鼠标移动时确定其位置,或者为了得到能够触发Paint event
的目标的Graphics
属性?事实上,我们通过继承自EventArgs
的类为使用者提供数据。例如,在我们的例子中,我们想提供已经到达了的数字。下面是类的定义:
public class NumberReachedEventArgs : EventArgs
{
private int _reached;
public NumberReachedEventArgs(int num)
{
this._reached = num;
}
public int ReachedNumber
{
get
{
return _reached;
}
}
}
如果没有必要提供使用者任何的信息,只需要使用EventArgs
类就可以了。
现在,所有的准备工作都已经做好了,让我们深入到Conter
类中:
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
}
}
}
在上述代码中,当到达一个既定的数字的时候,我们触发了一个事件。此时需要考虑很多事情:
- 触发事件是通过调用我们的
event
实现的(名为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
修饰,这就意味着他对于那些继承自该类的子类是有效的。 - 这个方法同时也是
virtual
,这意味着他可以被子类重写。
这种方式很有用。假设你设计了一个类,它继承自Counter
类。通过重写OnNumberReached
方法,在你的类中,可以在事件触发之前做一些额外的工作。例如:
protected override void OnNumberReached(NumberReachedEventArgs e)
{
//Do additional work
base.OnNumberReached(e);
}
注意,要是你不调用base.OnNumberReached(e)
,事件将永远不会被触发。这在你继承了一些类,但是又想屏蔽他的一些事件的时候会有一定用处,很有意思的技巧。
在实际的例子中,你可以只建立一个新的ASP.NET网页应用,然后看看后台生成的代码。你可以看到你的页面继承自System.Web.UI.Page
类。这个类有个名叫OnInit
,用virtual
和protected
修饰的方法。这个重写的方法中额外调用了InitializeComponent()
方法,OnInit(e)
在基类中被调用。
#region Web Form Designer generated code
protected override void OnInit(EventArgs e)
{
//CODEGEN: This call is required by the ASP.NET Web Form Designer.
InitializeComponent();
base.OnInit(e);
}
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.Load += new System.EventHandler(this.Page_Load);
}
#endregion
- 注意,
NumberReachedEventHandler
委托在类外,但是在这个命名空间内被定义了,对于所有在这个命名空间中的类都是有效的。
好了,现在是实践我们的Counter
类的时候了。
在我们的示例程序中,有两个名为texCounterTo
和txtReachable
的textboxes
空间,如下:
下面是btnRun
click event的event handler:
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());
}
如下可以为一些事件初始化event handler:
oCounter.NumberReached += new NumberReachedEventHandler( oCounter_NumberReached);
现在你能理解我们在这里做什么了!你刚刚实例化了一个类型为NumberReachedEventHandler
的委托,注意oCounter_NumberReached
的标记,要和我之前提到的一样。
同时注意我们使用了+=
,而不是=
。
这是因为委托是个很特殊的对象,它可以引用不止一个对象(这里引用了不止一个函数)。例如你有另一个名叫oCounter_NumberReached2
的函数,它们两个都可以通过下面的方式被引用:
oCounter.NumberReached += new NumberReachedEventHandler(
oCounter_NumberReached);
oCounter.NumberReached += new NumberReachedEventHandler(
oCounter_NumberReached2);
现在,当事件触发的时候,这两个函数会依次被执行。
如果在你代码的某些地方,基于一些条件,你希望当NumberReached
事件发生的时候 oCounter_NumberReached2
不被执行,可以通过下面简单的一个语句实现:
oCounter.NumberReached -= new NumberReachedEventHandler(
oCounter_NumberReached2);
event关键字
很多人一直在问:如果不用event
这个关键字会怎么样呢?
本质上,声明一个event
关键字是为了防止委托的使用者将委托设置为null
。为什么这个如此重要?试想一下,一个用户在委托的调用中列了我的类中的函数,另一个用户也是。目前一切都很好,但是如果有个人不是使用+=
,而是用=
单纯的把委托绑定到了一个新的回调函数上。这就是丢弃了原来旧的委托和它的调用列表。当时间到的时候,所有其他的客户都将无法收到他们的回调。这就是一种event
关键字能够解决问题的情形。如果我将event
关键字放在Counter
类前,并且尝试去编译如下的代码,会产生一个编译错误。把event
关键字移除,这个错误就不会发生了。
结论:event
关键字的声明,能够为委托的实例增加一层保护,这层保护可以防止委托的使用者将委托和委托的调用列表重置,并且只允许在调用列表中添加或者删除目标。