.Net事件与委托

来源:互联网 发布:杭州城市骑行大数据 编辑:程序博客网 时间:2024/05/23 18:08

原文地址: http://space.itpub.net/14325734/viewspace-450242

【简介】

在前述的博文中我们总是提到事件和委托,而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 WindowsFormsApplication1
{
    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);
            }
        }
    }
}

在上述的代码中,当到达指定的数值时事件就会被触发。这里有许多事情需要考虑:
# 通过调用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)
{
        //做一些额外的工作
        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;
        Counter = 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("事件1已到达: " + 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关键字是委托实例的一个保护层,它可以阻止对委托进行清空操作,而只是允许向委托调用列表添加和移除对象。

本文示例程序在Windows xp sp3 + .Net 2.0 + vs2008 sp1下编译调试通过。

【下载本文附带源码】
原创粉丝点击