原文
标题:Events and Delegates simplified
作者:Maysam Mahfouzi.
链接:
http://www.codeproject.com/csharp/events.asp翻译完成时间:2007-11-19
内容摘要
- 介绍
- 什么是委托
- 理解事件
- event 关键字
- 最后
介绍【Introduction】
当我在学习事件和委托的概念时,我读了大量的文章想去完全地明白它们是怎么使用的;现在,我想在这里把我所有学的东西呈现在此,同时它们大部分也是你需要学习的知识。
什么是委托【What are delegates?】
委托和时间两个概念是紧密关联的。委托只是一个函数指针(function pointers),也就是说,他们拥有方法(function)的引用。
委托是一个类。当你创建一个实例化时,你要传入一个这个委托所要引用的方法(function)的名称(将作为委托的构造函数的参数)。
每个委托都拥有一个签名(signature),如:
Delegate int SomeDelegate(string s, bool b);
是一个委托声明。我说这个委托拥有签名,这个签名的意思就是它表示返回一个int类型和携带2个参数,类型为string和bool。
我说,当你使用委托时,你需要传入一个这个委托所要引用的方法的名称作为它的构造函数参数。重要的一点是,只有拥有相同参数的方法才能作为委托的参数传入。
考虑以下方法:
private int SomeFunction(string str, bool bln){}
你可以传入这个方法作为SomeDelegate的构造函数,因为他们拥有相同的签名。
SomeDelegate sd = new SomeDelegate(SomeFunction);
现在,sd 引用了 SomeFunction;换句话说,SomeFunction 被注册到(registered to)sd。如果你调用sd,SomeFunction 将被调用。先记住我所说的“注册方法”(registered functions)。后面,我会引用注册方法。
sd("somestring", true);
现在你明白了怎么使用委托,让我们继续看看事件。
理解事件【Understanding Events】
- Button是一个类,当你点击它的时候,click事件被触发。
- Timer是一个类,每一毫秒,tick事件都被触发。
想知道怎么回事吗?我们通过一个例子学习:
一个假设情节(scenario):我们有一个名为Counter的类。这个类有一个方法用来计算0到countTo的方法,名为CountTo(int countTo, int reachableNum);以及每当计算到reachableNum就会引发NumberReached 事件。
我们的类有一个事件:NumberReached。事件是委托类型的变量。如果你需要声明一个事件,你只需要声明一个委托类型变量,并填写event关键字在你的声明处,如下所示:
public event NumberReachedEventHandler NumberReached;
在上面的声明中,NumberReachedEventHandler 只是一个委托。可能这样说比较好:NumberReachedDelegate,但注意,Microsoft 不会说MouseDelegate 或 PaintDelegate,取而代之的是MouseEventHandler 和 PaintEventHandler。那我们就可以方便地说用NumberReachedEventHandler 代替NumberReachedDelegate。好吗?Good!
在我们声明事件之前,我们需要定义我们的委托(我们的事件句柄(event handler))。如下图所示:
public delegate void NumberReachedEventHandler(object sender,
NumberReachedEventArgs e);
就如你看的,我们的委托名称为:NumberReachedEventHandler,并且它的签名包括一个void的返回值和两个参数,参数类型分别为 object 和 NumberReachedEventArgs。如果你想在某处实例化这个委托,那么这个方法必须有相同的签名才能传入作为委托的构造函数参数。
你曾经在你的代码里面用过PaintEventArgs 或 MouseEventArgs ,去判断鼠标的位置,移到哪里,或者判断哪个对象触发了Paint 事件?实际上,我们为客户端提供的数据都继承于EventArgs 类。举例来说,在我们上面的例子,我们要提供一个需要到达的数字,我们就会这样定义类。
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);
public class Counter
{
public event NumberReachedEventHandler NumberReached;
public Counter()
{
}
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;//不再计算
}
}
}
protected virtual void OnNumberReached(NumberReachedEventArgs e)
{
if(NumberReached != null)
{
NumberReached(this, e);//引发事件
}
}
}
}
在以上代码,我们如果计算到达了期望值就会引发事件。这里有几点需要考虑:
1、完成“触发事件”是通过呼叫我们的事件(一个名称为NumberReachedEventHandler的委托实例):
NumberReached(this, e);
这样,所有注册方法都会被调用。
2、我们通过这样为注册方法提供数据:
NumberReachedEventArgs e = new NumberReachedEventArgs(reachableNum);
3、提一个问题:为什么我们要通过OnNumberReached(NumberReachedEventArgs e)调用NumberReached(this, e)。为什么部门不直接像下面的代码那样调用。
if(ctr == reachableNum)
{
NumberReachedEventArgs e = new NumberReachedEventArgs(reachableNum);
//OnNumberReached(e); //之前的做法
if(NumberReached != null) //新做法
{
NumberReached(this, e); //引发事件
}
return;//不再计算
}
提得好!如果你想知道为什么我们不直接调用,请再看看OnNumberReached的签名。
protected virtual void OnNumberReached(NumberReachedEventArgs e)
- 这个方法是protected的,意味子类也可以使用。
- 这个方法是virtual,意味着可以被子类重写。
以上两点是很有用的。想象一下,你要设计一个类继承于Counter 。通过重写OnNumberReached 方法,你既可以在引发事件之前,在你要设计的类里面添加一些额外的工作。如:
protected override void OnNumberReached(NumberReachedEventArgs e)
{
//做额外工作
base.OnNumberReached(e);
}
注意,如果你没有呼叫base.OnNumberReached(e),那么事件将永远不会被触发!当你想在子类里面除去它的事件时这样做就非常有用了。有趣的窍门呢,哈哈。
在实际使用中,你可以创建一个新的ASP.NET 网页应用程式,并看看里面生成的后台代码。如你所见,你的page继承自System.Web.UI.Page 类。这个类有一个virtual 和 protected 的方法名为OnInit。你看到在重写的方法中调用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
4、注意,NumberReachedEventHandler 委托定义在类的外面,在命名空间之内,可见于所有类。
好的。到时间实践使用一下我们的Counter 类:
在我们示例应用程式中,我们有2个textbox,命名分别为txtCountTo 和 txtReachable ,如图所示
这里就是处理btnRun 的Click事件的代码:
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_NumberReached 有相同签名的方法oCounter_NumberReached2 ,2个方法同时可以通过以下方法被引用:
oCounter.NumberReached += new NumberReachedEventHandler(
oCounter_NumberReached);
oCounter.NumberReached += new NumberReachedEventHandler(
oCounter_NumberReached2);
现在,引发事件之后,两个方法会被依次调用。
如果在我们代码的某处,基于某些原因,我们决定oCounter_NumberReached2 引发NumberReached事件时不需要再调用,我们可以简单地如下这样做:
oCounter.NumberReached -= new NumberReachedEventHandler(
oCounter_NumberReached2);
event关键字【event Keyword】
基本上,声明event可以防止delegate设置为null。为什么这个这么重要?想象我现在已经添加一个委托调用列表到我们的类。这样,现在其他客户端可以运行得很好。忽然,有某人简单的使用了“=”代替了“+=”设置新的回调。这样就会抛弃了所有旧的委托,并且创建一个完全全新的委托到委托调用列表,而这样调用列表中只有它一项而已。这样,当时间到的时候,其他客户就不能收到他们回调函数。这是event 关键字所要解决的一种状况。如果我们在Counter类中移除了event关键,然后尝试编译下面的代码,会出现以下错误:
结论:事件的生命为实例化的委托添加了一层保护。这个保护防止客户端重置委托和它的调用列表,只能允许在调用列表中添加或移除对应目标。[引用http://weblogs.asp.net/rosherove/archive/2004/03/28/100444.aspx]
最后【Finally】
不要忘记在你的应用程式的主构造函数中命名下列代码,代替在cmdRun_Click 事件内定义。我定义他们到cmdRun_Click 事件处理内只是为了方便而已。
public Form1()
{
//
// Required for Windows Form Designer support
//
InitializeComponent();
//
// TODO: Add any constructor code after InitializeComponent call
//
oCounter = new Counter();
oCounter.NumberReached += new NumberReachedEventHandler(
oCounter_NumberReached);
oCounter.NumberReached += new NumberReachedEventHandler(
oCounter_NumberReached2);
}
文章的源代码在上面提供下载。
如果你投票少于5分的话,请让我知道原因;-)。希望你读完这篇文章后会满意。