Nazarite

导航

谈谈事件与委托

简单谈谈事件与委托

翻译:博客园-孙悟天 http://www.cnblogs.com/edgar-sun/archive/2007/04/08/704335.html

原文地址:http://www.codeproject.com/csharp/events.asp
源代码下载:/Files/edgar-sun/events_src.zip
演示文件下载:/Files/edgar-sun/events_demo.zip
作者:Maysam Mahfouzi  
原文发布日期:2003/8/16
原文更新日期:2005/5/14

内容


介绍
什么是委托?
理解事件
event关键字
结尾

 

介绍
   当我设法学习事件与委托时,我阅读了很多文章去完全地理解它们并使用它们,现在我想把我学到的展现在这里,其中有很多知识你也需要学习。
什么是委托?
   委托和事件是紧紧联系在一起的。委托是函数(方法)指针,更确切地说,委托保持方法的引用。
   委托是一个类。当你创建它的实例的时候,你传递将被委托调用的方法名(做为委托构造器的参数)。
   每个委托都有一个特征。例如:

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);
   既然你已经知道怎么使用委托,下面让我们来理解事件……

理解事件
   一个按钮是一个类,当你点击它的时候,click事件被触发。
   一个计时器是一个类,每毫秒触发一个tick事件。
   想要知道发生什么了?让我们通过一个例子去学习:
   这是一个假定:我们有一个类Counter。这个类有一个CountTo(int counTo,int reachableNum)方法,从0到countTo计数,并且只要计数到reachableNum这个数时会触发一个NumberReached事件。
   我们的类有一个事件:NumberReached。事件是委托的变量。我的意思是,如果你声明一个事件,同时也声明某一类型的委托,并且要把event关键字放在声明前面,看起来应该是这样的:

public event NumberReachedEventHandler NumberReached;
   在上面的声明中,NumberReachedEventHandler只是一个委托。也许它更应该叫NumberReachedDelegate,但是注意到微软并没叫MouseDelegate或PaintDelegate,而是叫MouseEventHandler或PaintEventHandler。把它命名为NumberReachedEventHandler而不是NumberReachedDelegate,这只是一个惯例,了解?很好!
   你了解了我们声明事件前,需要先定义相应的委托(事件处理者)。它看起来可能是这样的:

public delegate void NumberReachedEventHandler(object sender,
    NumberReachedEventArgs e);
   如你所见,我们委托的名字是:NumberReachedEventHandler,它的特征是返回一个void值,两个参数类型分别为object和NumberReachedEventArgs。如果你在某处实例化这个委托时,你所传递的方法必须和它具有一样的特征。
   在你的代码中,你使用过MouseEventArgs或PaintEventArgs去确定鼠标的位置,它在向哪移动,或某个物体的图形属性触发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);

    /// <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
            }
        }
    }
   在上面的代码中,如果到达预期的数时就触发一个事件。在这里我们需要考虑很多事情:
   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);//Raise the event
    }
    return;//don't count any more
}
   好问题!如果你想知道为何间接调用,请看OnNumberReached方法的特征:

protected virtual void OnNumberReached(NumberReachedEventArgs e)
      你看到了,它是保护方法,意味着从这个类继承的类(子类)中它是可以调用的。
      同时它也是虚方法,意味着子类可以改写它的实现。
那是非常有用的。想象一下你正在设计一个从Counter类继承的类,通过改写OnNumberReached方法,可以在事件触发之前方便的增加一些附加的工作。例如:

protected override void OnNumberReached(NumberReachedEventArgs e)
{
    //Do additional work
    base.OnNumberReached(e);
}
   注意如果你不调用base.OnNumberReached(e),那么事件永远也不会触发。当你继承了一些类时,你可能想要去除一些事件,这时这样也许就有用了。有趣的窍门,哈?
   一个真实的例子,当你创建一个新的ASP.NET 应用程序时,你去看看后台产生的代码,你会发现你的页面继承自System.Web.UI.Page类,而且有一个叫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类外,Events命名空间内的,对所有类都可见。
   好了,是时候实践一下我们的Counter类了。
   在我们的应用程序中,我们有两个文本框:txtCountTo和txtReachable

   这里是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);
   现在你应该了解到我们正在做什么。我们初始化了NunberReachedEvnetHandler委托(也可以对其他对象)。注意我上面提及的oCounter_NumberReached方法签名的相似性。
   现在来看看我们用=代替+=的情形。
   委托是特殊的对象,因为它可以保持多个对象的引用(这里是多个方法)。例如,如果你有另一个方法叫oCounter_NumberReached2,而且签名和oCounter_NumberReached一样,那么两个方法都可以象下面那样引用:

oCounter.NumberReached += new NumberReachedEventHandler(
    oCounter_NumberReached);
oCounter.NumberReached += new NumberReachedEventHandler(
    oCounter_NumberReached2);
   现在,当事件被触发,一个接一个的方法将被调用。
   如果在你的代码某处,你不想在NumberReached事件触发时调用oCounter_NumberReached2,你可以这样做:

oCounter.NumberReached -= new NumberReachedEventHandler(
    oCounter_NumberReached2);
event关键字
   许多人也许会问:如果我们不用event关键字会怎么样?
   使用evnet关键字可以阻止任何一个委托的使用者把它设为null。为什么这是重要的?想象一下,一个客户把我类中的其中一个方法注册到委托调用链表,其他客户也这样做,这不会出错。现在,如果有另一客户用=代替+=给委托新注册一个方法。这将会把原来的委托调用链表清空,并且创建一个全新的单一的委托在委托调用链表中。这时其他客户将无法接收回复信息。关键字event正是针对这一问题提出的,如果我在Counter类中加上event关键字,并试着编译下面的代码,将产生一个编译器错误信息:

   总之,event关键字在委托实例上加了一层保护,保护客户的委托以免被重新设置及委托调用链被清空,这样就只允许对委托调用链进行添加或移除操作。
结尾
   别忘了在你应用程序的构造函数中声明以下内容,而不是在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);
   提供的源代码就是这样子的。


 

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/xychen2008/archive/2007/04/26/1585238.aspx

 

 

C#委托机制应用例解 

       事件与委托似乎很难以理解,这是因为它们的使用方式与常用的编码有很大的差别,例如通常编写的都是同步代码,调用一个类型的方法,会即刻出现方法执行的结果,这是符合逻辑的。但在某些情况中,同步代码未必满足需求,拿公共汽车来打个比方,如果交通管制中心希望每一辆公车到达一个站点时都发送给自己一个信号以便自己能够随时掌握交通状况,使用同步代码,公汽对象肯定需要调用管制中心对象,这样就出现了我们一直不愿意看到的情况:两个类型紧密地耦合在一起。既然要其它类型对自己的行为作出反应,亲自调用其类型的方法似乎不可避免,在同步代码中,很难避免这种紧密的类型调用关系。

另一个差别是在一般情况下,我们只将属性作为参数传递给方法,而很少会考虑将一个方法传递给另一个方法。

我们抛弃各种C#参考书中桀骜难懂的事件与委托概念,设想一个情景来理解事件与委托的使用:有一家IT公司,董事长不希望自己的雇员在上班时间玩游戏,但又不可能每时每刻都盯着每个雇员,因此,他希望使用一种新的方式实现监视雇员的效果:如果有雇员违反规定,某个设备或专门的监查人员将自动发出一个消息通知他,董事长只需要在事情发生时进行处理。

因此,这个用例实际上是两种类型——董事长类与雇员类——之间的交互,下面的代码将给读者展示如何使用委托与事件机制实现这种交互:

首先,我们需要在董事长类与雇员类之间定义一个委托类型,用于传递两者之间的事件,这个类型就是一个监视设备或专门负责打小报告的监查人员:

public delegate void DelegateClassHandle();

定义一个委托的过程类似方法的定义,但它没有方法体。定义委托一定要添加关键字delegate。由于定义委托实际上相当一个类,因此可以在定义类的任何地方定义委托。另外,根据委托的可见性,也可以添加一般的访问修饰符,如public、private和protected。

委托的返回值类型为void,这并非表示委托类型本身带有返回值,该返回值类型是指委托的目标函数类型,即它委托的一个事件处理函数返回值是void类型。

新建一个雇员类Employee,其代码如下:

public class Employee
{
    public event DelegateClassHandle PlayGame;
 
    public void Games()
    {
        if (PlayGame != null)
        {
            PlayGame();
        }
    }
}
雇员类Employee代码中定义了一个DelegateClassHandle类型的事件PlayGame,它的定义方式也很特殊,首先必须使用关键字event,表示PlayGame是一个事件,同时还必须声明该事件的委托类型为DelegateClassHandle,即将来由该类型的委托对象负责通知事件。
如果有雇员开始玩游戏,它将执行Games方法,而只要该方法一被调用,就会触发一个事件PlayGame,然后董事长就会收到这个事件的消息——有人在玩游戏了。
董事长类代码如下,他有一个方法Notify用于接收消息:
public class Admin
{
    public void Notify()
    {
        System.Console.WriteLine("someone is playing game");
    }
}
Employee的PlayGame事件如何与Admin的Notify方法关联起来呢?只需通过事件绑定即可实现,具体过程如下列代码:
Employee employee = new Employee();
Admin admin = new Admin();
 
employee.PlayGame += new DelegateClassHandle(admin.Notify);
employee.Games();
请大家注意事件绑定的代码:
employee.PlayGame += new DelegateClassHandle(admin.Notify);
通过DelegateClassHandle将两个类的交互进行了绑定,当employee.Games方法调用后,触发PlayGame事件,而该事件将被委托给admin的Notify方法处理,通知董事长有雇员在上班时间玩游戏。
但董事长并不满足这种简单的通知,他还想知道究竟是谁在上班时间违反规定。显然,现在委托对象必须传递必要的参数才行,这个要求也可以很容易地办到。事件的参数可以设置为任何类型的数据,在.NET框架中,还提供了事件参数基类EventArgs专门用于传递事件数据。
从该EventArgs类派生一个自定义的事件参数类CustomeEventArgs,这个类型将携带雇员姓名和年龄信息:
public class CustomeEvetnArgs : EventArgs
{
    string name = "";
    int age = 0;
    public CustomeEvetnArgs()
    { }
    public string Name
    {
        get { return this.name; }
        set { this.name = value; }
    }
    public int Age
    {
        get { return this.age; }
        set { this.age = value; }
    }
}
修改委托类型DelegateClassHandle的定义,让其携带必要的参数:
public delegate void DelegateClassHandle(object sender, CustomeEvetnArgs e);
雇员类的代码修改后如下:
public class Employee
{
    private string _name;
 
    public string Name
    {
        get { return _name; }
        set { _name = value; }
    }
    private int _age;
 
    public int Age
    {
        get { return _age; }
        set { _age = value; }
    }
 
    public event DelegateClassHandle PlayGame;
 
    public void Games()
    {
        if (PlayGame != null)
        {
            CustomeEvetnArgs e = new CustomeEvetnArgs();
            e.Name = this._name ;
            e.Age = this._age;
            PlayGame(this, e);
        }
    }
}
在Games方法中,首先新建一个CustomeEventArgs对象,然后设置了必要的属性Name和Age。
董事长的通知方法也必须相应地进行修改:
public class Admin
{
    public void Notify(object sender, CustomeEvetnArgs e)
    {
        System.Console.WriteLine(e.Name+" is "+e.Age.ToString());
    }
}
将两个类型对象进行关联的代码也需要进行相应的修改:
Employee employee = new Employee();
employee.Name = "Mike";
employee.Age = 25;
Admin admin = new Admin();
 
employee.PlayGame += new DelegateClassHandle(admin.Notify);
employee.Games();
修改后的代码运行的结果是,当Mike调用Games方法玩游戏时,会自动触发PlayGame事件,而该事件携带相关信息通知admin,后者的Notify方法将接收到数据并输出“Mike is 25”,告诉董事长是Mike,25岁,正在上班时间玩游戏。
 
委托是可以多路广播(Mulitcast)的,即一个事件可以委托给多个对象接收并处理。在上面的用例中,如果有另一位经理与董事长具有同样的癖好,也可以让委托对象将雇员的PlayGame事件通知他。
首先定义经理类:
public class Manager
{
    public void Notify(object sender, CustomeEvetnArgs e)
    {
        System.Console.WriteLine(sender.ToString() + "-" + e.Name);
    }
}
经理Manager类型的Notify方法与Admin一致,他也接受到相应的信息。
委托的多路广播绑定的方法仍然是使用+=运算符,其方法如下面的代码所示:
Employee employee = new Employee();
employee.Name = "Mike";
employee.Age = 25;
Admin admin = new Admin();
Manager manager = new Manager();
 
employee.PlayGame += new DelegateClassHandle(admin.Notify);
employee.PlayGame += new DelegateClassHandle(manager.Notify);
employee.Games();
执行该方法,读者将看到admin和manager的Notify方法都会被事件通知并调用执行。通过这样的方法,董事长和经理都会知道Mike在玩游戏了。

如果董事长不希望经理也收到这个通知,该如何解除PlayGame对manager的事件绑定呢?同样非常简单,在employee.Games方法被调用前执行下列语句即可:

employee.PlayGame -= new DelegateClassHandle(manager.Notify);

 最后需要提醒读者注意的,Employee类中的Games方法在触发事件PlayGame之前需要判断该事件是否为null。当employee对象的Games方法触发事件PlayGame后,必须有一个目标函数来处理这个事件,而该语句正是判断该目标函数是否存在。如果将这个判断去掉,且对事件不进行任何绑定而直接调用Games方法,程序将在事件PlayGame处弹出一个NullReferenceException的异常。

读者能够从委托与事件的代码中得出什么结论吗?两个需要存在调用关系的类型,在各自的实现中却没有编写实际的调用代码,它们只是通过一个事件和一个第三方的委托类型完成了消息的传递过程。两个类型之间不存在任何的紧密耦合,它们看似松散地通过一个委托对象中通信,实现了本书一直宣传的“高聚合”和“低耦合”观点。

 

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/yap111/archive/2008/02/20/2110544.aspx

 

 

posted on 2009-12-28 14:26  Nazarite  阅读(231)  评论(0编辑  收藏  举报