C#委托和事件(Delegate、Event、EventHandler、EventArgs)

14.1、委托

当要把方法作为实参传送给其他方法的形参时,形参需要使用委托。委托是一个类型,是一个函数指针类型,这个类型将该委托的实例化对象所能指向的函数的细节封装起来了,即规定了所能指向的函数的签名,也就是限制了所能指向的函数的参数和返回值。当实例化委托的时候,委托对象会指向某一个匹配的函数,实质就是将函数的地址赋值给了该委托的对象,然后就可以通过该委托对象来调用所指向的函数了。利用委托,程序员可以在委托对象中封装一个方法的引用,然后委托对象作为形参将被传给调用了被引用方法的代码,而不需要知道在编译时刻具体是哪个方法被调用。

一般的调用函数,我们都不会去使用委托,因为如果只是单纯的调用函数,使用委托更麻烦一些;但是如果想将函数作为实参,传递给某个函数的形参,那么形参就一定要使用委托来接收实参,一般使用方法是:在函数外面定义委托对象,并指向某个函数,再将这个对象赋值给函数的形参,形参也是该委托类型的对象变量,函数里面再通过形参来调用所指向的函数。

14.1.1、定义委托

语法如下:

delegate result-type Identifier ([parameters]);

说明:

result-type:返回值的类型,和方法的返回值类型一致

Identifier:委托的名称

parameters:参数,要引用的方法带的参数

小结:

当定义了委托之后,该委托的对象一定可以而且也只能指向该委托所限制的函数。即参数的个数、类型、顺序都要匹配,返回值的类型也要匹配。

因为定义委托相当于是定义一个新类,所以可以在定义类的任何地方定义委托,既可以在一个类的内部定义,那么此时就要通过该类的类名来调用这个委托(委托必须是public、internal),也可以在任何类的外部定义,那么此时在命名空间中与类的级别是一样的。根据定义的可见性,可以在委托定义上添加一般的访问修饰符:当委托定义在类的外面,那么可以加上public、internal修饰符;如果委托定义到类的内部,那么可以加上public、 private、 protected、internal。一般委托都是定义在类的外面的。

14.1.2、实例化委托

Identifier objectName = new Identifier( functionName )

实例化委托的实质就是将某个函数的地址赋值给委托对象。在这里:

Identifier :这个是委托名字。

objectName :委托的实例化对象。

functionName:是该委托对象所指向的函数的名字。对于这个函数名要特别注意:定义这个委托对象肯定是在类中定义的,那么如果所指向的函数也在该类中,不管该函数是静态还是非静态的,那么就直接写函数名字就可以了;如果函数是在别的类里面定义的public、internal,但是如果是静态,那么就直接用类名.函数名,如果是非静态的,那么就类的对象名.函数名,这个函数名与该对象是有关系的,比如如果函数中出现了this,表示的就是对当前对象的调用。

14.1.3、委托推断

C# 2.0用委托推断扩展了委托的语法。当我们需要定义委托对象并实例化委托的时候,就可以只传送函数的名称,即函数的地址:

Identifier objectName = functionName;

这里面的functionName与14.1.2节中实例化委托的functionName是一样的,没什么区别,满足上面的规则。

C#编译器创建的代码是一样的。编译器会用objectName检测需要的委托类型,因此会创建Identifier委托类型的一个实例,用functionName即方法的地址传送给Identifier的构造函数。

注意:

不能在functionName后面加括号和实参,然后把它传送给委托变量。调用方法一般会返回一个不能赋予委托变量的普通对象,除非这个方法返回的是一个匹配的委托对象。总之:只能把相匹配的方法的地址赋予委托变量。

委托推断可以在需要委托实例化的任何地方使用,就跟定义普通的委托对象是一样的。委托推断也可以用于事件,因为事件基于委托(参见本章后面的内容)。

14.1.4、匿名方法

到目前为止,要想使委托工作,方法必须已经存在。但实例化委托还有另外一种方式:即通过匿名方法。

用匿名方法定义委托的语法与前面的定义并没有区别。但在实例化委托时,就有区别了。下面是一个非常简单的控制台应用程序,说明了如何使用匿名方法:

using System;

namespace Wrox.ProCSharp.Delegates
{

    class Program
    {

        delegate string DelegateTest(string val);

        static void Main()
        {

            string mid = ", middle part,";

            //在方法中定义了方法

            DelegateTest anonDel = delegate(string param)
            {

                param += mid;

                param += " and this was added to the string.";

                return param;

            };

            Console.WriteLine(anonDel("Start of string"));

        }

    }

}
using System;

namespace Wrox.ProCSharp.Delegates
{

    class Program
    {

        delegate string DelegateTest(string val);

        static void Main()
        {

            string mid = ", middle part,";

            //Lamada表示法

            DelegateTest anonDel = ( param)=>
            {

                param += mid;

                param += " and this was added to the string.";

                return param;

            };

            Console.WriteLine(anonDel("Start of string"));

        }

    }

}

委托DelegateTest在类Program中定义,它带一个字符串参数。有区别的是Main方法。在定义anonDel时,不是传送已知的方法名,而是使用一个简单的代码块:它前面是关键字delegate,后面是一个参数:

            DelegateTest anonDel = delegate(string param)
            {

                param += mid;

                param += " and this was added to the string.";

                return param;

            };

匿名方法的优点是减少了要编写的代码。方法仅在有委托使用时才定义。在为事件定义委托时,这是非常显然的。(本章后面探讨事件。)这有助于降低代码的复杂性,尤其是定义了好几个事件时,代码会显得比较简单。使用匿名方法时,代码执行得不太快。编译器仍定义了一个方法,该方法只有一个自动指定的名称,我们不需要知道这个名称。

在使用匿名方法时,必须遵循两个规则:

1、在匿名方法中不能使用跳转语句跳到该匿名方法的外部,反之亦然:匿名方法外部的跳转语句不能跳到该匿名方法的内部。

2、在匿名方法内部不能访问不安全的代码。另外,也不能访问在匿名方法外部使用的ref和out参数。但可以使用在匿名方法外部定义的其他变量。方法内部的变量、方法的参数可以任意的使用。

如果需要用匿名方法多次编写同一个功能,就不要使用匿名方法。而编写一个指定的方法比较好,因为该方法只需编写一次,以后可通过名称引用它。

14.1.5、多播委托

前面使用的每个委托都只包含一个方法调用,调用委托的次数与调用方法的次数相同,如果要调用多个方法,就需要多次给委托赋值,然后调用这个委托。

委托也可以包含多个方法,这时候要向委托对象中添加多个方法,这种委托称为多播委托,多播委托有一个方法列表,如果调用多播委托,就可以连续调用多个方法,即先执行某一个方法,等该方法执行完成之后再执行另外一个方法,这些方法的参数都是一样的,这些方法的执行是在一个线程中执行的,而不是每个方法都是一个线程,最终将执行完成所有的方法。

如果使用多播委托,就应注意对同一个委托调用方法链的顺序并未正式定义,调用顺序是不确定的,不一定是按照添加方法的顺序来调用方法,因此应避免编写依赖于以特定顺序调用方法的代码。如果要想确定顺序,那么只能是单播委托,调用委托的次数与调用方法的次数相同。

多播委托的各个方法签名最好是返回void;否则,就只能得到委托最后调用的一个方法的结果,而最后调用哪个方法是无法确定的。

多播委托的每一个方法都要与委托所限定的方法的返回值、参数匹配,否则就会有错误。

我自己写代码测试,测试的结果目前都是调用顺序和加入委托的顺序相同的,但是不排除有不同的时候。

delegate result-type Identifier ([parameters]);

14.1.5.1、委托运算符 =

Identifier objectName = new Identifier( functionName);

或者

Identifier objectName = functionName;

这里的“=”号表示清空 objectName 的方法列表,然后将 functionName 加入到 objectName 的方法列表中。

14.1.5.2、委托运算符 +=

objectName += new Identifier( functionName1);

或者

objectName += functionName1;

这里的“+=”号表示在原有的方法列表不变的情况下,将 functionName1 加入到 objectName 的方法列表中。可以在方法列表中加上多个相同的方法,执行的时候也会执行完所有的函数,哪怕有相同的,就会多次执行同一个方法。

注意:objectName 必须是已经赋值了的,否则在定义的时候直接使用该符号:

Identifier objectName += new Identifier( functionName1);或者

Identifier objectName += functionName1;就会报错。

14.1.5.3、委托运算符 -=

objectName -= new Identifier(functionName1);

或者

objectName -= functionName1;

这里的“-=”号表示在 objectName 的方法列表中减去一个functionName1。可以在方法列表中多次减去相同的方法,减一次只会减一个方法,如果列表中无此方法,那么减就没有意义,对原有列表无影响,也不会报错。

注意:objectName 必须是已经赋值了的,否则在定义的时候直接使用该符号:

Identifier objectName -= new Identifier( functionName1);或者

Identifier objectName -= functionName1;就会报错。

14.1.5.4、委托运算符 +、-

Identifier objectName = objectName + functionName1 - functionName1;

或者

Identifier objectName = new Identifier(functionName1) + functionName1 - functionName1;

对于这种+、-表达式,在第一个符号+或者-的前面必须是委托而不能是方法,后面的+、-左右都随便。这个不是绝对规律,还有待进一步的研究。

14.1.5.5、多播委托的异常处理

通过一个委托调用多个方法还有一个大问题。多播委托包含一个逐个调用的委托集合。如果通过委托调用的一个方法抛出了异常,整个迭代就会停止。下面是MulticastIteration示例。其中定义了一个简单的委托DemoDelegate,它没有参数,返回void。这个委托调用方法One()和Two(),这两个方法满足委托的参数和返回类型要求。注意方法One()抛出了一个异常:

using System;

namespace Wrox.ProCSharp.Delegates
{

    public delegate void DemoDelegate();

    internal class Program
    {
        private static void One()
        {

            Console.WriteLine("One");

            throw new Exception("Error in one");

        }

        private static void Two()
        {

            Console.WriteLine("Two");

        }

        static void Main()
        {

            DemoDelegate d1 = One;

            d1 += Two;

            try
            {

                d1();

            }

            catch (Exception)
            {

                Console.WriteLine("Exception caught");

            }
            Console.ReadKey();
        }

    }

}

在Main()方法中,创建了委托d1,它引用方法One(),接着把Two()方法的地址添加到同一个委托中。调用d1委托,就可以调用这两个方法。异常在try/catch块中捕获。

委托只调用了第一个方法。第一个方法抛出了异常,所以委托的迭代会停止,不再调用Two()方法。当调用方法的顺序没有指定时,结果会有所不同。

One

Exception Caught

注意:

多播委托包含一个逐个调用的委托集合。如果通过委托调用的一个方法抛出了异常,整个迭代就会停止。即如果任一方法引发了异常,而在该方法内未捕获该异常,则该异常将传递给委托的调用方,并且不再对调用列表中后面的方法进行调用。

在这种情况下,为了避免这个问题,应手动迭代方法列表。Delegate类定义了方法GetInvocationList(),它返回一个Delegate对象数组。现在可以使用这个委托调用与委托直接相关的方法,捕获异常,并继续下一次迭代。

using System;

namespace Wrox.ProCSharp.Delegates
{

    public delegate void DemoDelegate();

    internal class Program
    {
        private static void One()
        {

            Console.WriteLine("One");

            throw new Exception("Error in one");

        }

        private static void Two()
        {

            Console.WriteLine("Two");

        }

        static void Main()
        {

            DemoDelegate d1 = One;

            d1 += Two;

            Delegate[] delegates = d1.GetInvocationList();

            foreach (DemoDelegate d in delegates)
            {

                try
                {

                    d();

                }

                catch (Exception)
                {

                    Console.WriteLine("Exception caught");

                }

            }
            Console.ReadKey();
        }
    }

}

修改了代码后运行应用程序,会看到在捕获了异常后,将继续迭代下一个方法。

One

Exception caught

Two

注意:其实如果在多播委托的每个具体的方法中捕获异常,并在内部处理,而不抛出异常,一样能实现多播委托的所有方法执行完毕。这种方式与上面方式的区别在于这种方式的宜昌市在函数内部处理的,上面那种方式的异常是在函数外面捕获并处理的。

14.1.6、通过委托对象来调用它所指向的函数

1、委托实例的名称,后面的括号中应包含调用该委托中的方法时使用的参数。

2、调用委托对象的Invoke()方法,Invoke后面的括号中应包含调用该委托中的方法时使用的参数。

注意:实际上,给委托实例提供括号与调用委托类的Invoke()方法完全相同。因为Invoke()方法是委托的同步调用方法。

注意:不管是多播委托还是单播委托,在没有特殊处理的情况下,在一个线程的执行过程中去调用委托(委托对象所指向的函数),调用委托的执行是不会新起线程的,这个执行还是在原线程中的,这个对于事件也是一样的。当然,如果是在委托所指向的函数里面去启动一个新的线程那就是另外一回事了。

14.2、事件

14.2.1、自定义事件

14.2.1.1、声明一个委托:

Delegate result-type delegateName ([parameters]);

这个委托可以在类A内定义也可以在类A外定义。

14.2.1.2、声明一个基于某个委托的事件

Event delegateName eventName;

eventName不是一个类型,而是一个具体的对象,这个具体的对象只能在类A内定义而不能在类A外定义。

14.2.1.3、在类A中定义一个触发该事件的方法

ReturnType FunctionName([parameters])

{

……

If(eventName != null)

{

eventName([parameters]);

或者eventName.Invoke([parameters]);

}

……

}

触发事件之后,事件所指向的函数将会被执行。这种执行是通过事件名称来调用的,就像委托对象名一样的。

触发事件的方法只能在A类中定义,事件的实例化,以及实例化之后的实现体都只能在A类外定义。

14.2.1.4、初始化A类的事件

在类B中定义一个类A的对象,并且让类A对象的那个事件指向类B中定义的方法,这个方法要与事件关联的委托所限定的方法吻合。

14.2.1.5、触发A类的事件

在B类中去调用A类中的触发事件的方法:用A类的对象去调用A类的触发事件的方法。

14.2.1.6、程序实例

using System;

using System.Collections.Generic;
using System.Globalization;
using System.Text;

using System.Threading;

namespace DelegateStudy
{

    public delegate void DelegateClick(int a);

    public class Butt
    {

        public event DelegateClick Click;

        public void OnClick(int a)
        {

            if (Click != null)
                Click.Invoke(a);

            //Click(a);//这种方式也是可以的

            Console.WriteLine("Click()");

        }

    }

    class Program
    {
        public static void Btn_Click(int a)
        {

            for (long i = 0; i < a; i++)

                Console.WriteLine(i.ToString(CultureInfo.InvariantCulture));

        }
        static void Main(string[] args)
        {

            var b = new Butt();

            //在委托中,委托对象如果是null的,直接使用+=符号,会报错,但是在事件中,初始化的时候,只能用+=

            b.Click += Btn_Click; //事件是基于委托的,所以委托推断一样适用,下面的语句一样有效:b.Click += Fm_Click;

            //b.Click(10);错误:事件“DelegateStudy.Butt.Click”只能出现在 += 或 -= 的左边(从类型“DelegateStudy.Butt”中使用时除外)

            b.OnClick(10000);

            Console.ReadLine();

        }
    }
}

14.2.2、控件事件

基于Windows的应用程序也是基于消息的。这说明,应用程序是通过Windows来与用户通信的,Windows又是使用预定义的消息与应用程序通信的。这些消息是包含各种信息的结构,应用程序和Windows使用这些信息决定下一步的操作。

比如:当用户用鼠标去点击一个windows应用程序的按钮的时候,windows操作系统就会捕获到这个点击按钮的动作,这个时候它会根据捕获到的动作发送一个与之对应的预定义的消息给windows应用程序的这个按钮,windows应用程序的按钮消息处理程序会处理接收到的消息,这个程序处理过程就是根据收到的消息去触发相应的事件,事件被按钮触发后,会通知所有的该事件的订阅者来接收这个事件,从而执行相应的的函数。

在MFC等库或VB等开发环境推出之前,开发人员必须处理Windows发送给应用程序的消息。VB和今天的.NET把这些传送来的消息封装在事件中。如果需要响应某个消息,就应处理对应的事件。

14.2.2.1、控件事件委托EventHandler

在控件事件中,有很多的委托,在这里介绍一个最常用的委托EventHandler,.NET Framework中控件的事件很多都基于该委托,EventHandler委托已在.NET Framework中定义了。它位于System命名空间:

Public delegate void EventHandler(object sender,EventArgs e);

14.2.2.2、委托EventHandler参数和返回值

事件最终会指向一个或者多个函数,函数要与事件所基于的委托匹配。事件所指向的函数(事件处理程序)的命名规则:按照约定,事件处理程序应遵循“object_event”的命名约定。object就是引发事件的对象,而event就是被引发的事件。从可读性来看,应遵循这个命名约定。

首先,事件处理程序总是返回void,事件处理程序不能有返回值。其次是参数,只要是基于EventHandler委托的事件,事件处理程序的参数就应是object和EventArgs类型:

第一个参数接收引发事件的对象,比如当点击某个按钮的时候,这个按钮要触发单击事件最终执行这个函数,那么就会把当前按钮传给sender,当有多个按钮的单击事件都指向这个函数的时候,sender的值就取决于当前被单击的那个按钮,所以可以为几个按钮定义一个按钮单击处理程序,接着根据sender参数确定单击了哪个按钮:

if(((Button)sender).Name ==”buttonOne”)

第二个参数e是包含有关事件的其他有用信息的对象。

14.2.2.3、控件事件的其他委托

控件事件还有其他的委托,比如在窗体上有与鼠标事件关联的委托:

Public delegate void MouseEventHandler(object sender,MouseEventArgs e);

public event MouseEventHandler MouseDown;

this.MouseDown += new System.Windows.Forms.MouseEventHandler(this.Form1_MouseDown);

private void Form1_MouseDown(object sender, MouseEventArgs e){};

MouseDown事件使用MouseDownEventArgs,它包含鼠标的指针在窗体上的的X和Y坐标,以及与事件相关的其他信息。

控件事件中,一般第一个参数都是object sender,第二个参数可以是任意类型,不同的委托可以有不同的参数,只要它派生于EventArgs即可。

14.2.2.4、程序实例

using System;

using System.Collections.Generic;

using System.Text;

using System.Threading;

namespace SecondChangeEvent1
{

    // 该类用来存储关于事件的有效信息外,

    // 还用来存储额外的需要传给订阅者的Clock状态信息

    public class TimeInfoEventArgs : EventArgs
    {

        public TimeInfoEventArgs(int hour, int minute, int second)
        {

            this.hour = hour;

            this.minute = minute;

            this.second = second;

        }

        public readonly int hour;

        public readonly int minute;

        public readonly int second;

    }

    // 定义名为SecondChangeHandler的委托,封装不返回值的方法,

    // 该方法带参数,一个clock类型对象参数,一个TimeInfoEventArgs类型对象

    public delegate void SecondChangeHandler(

    object clock,

    TimeInfoEventArgs timeInformation

    );

    // 被其他类观察的钟(Clock)类,该类发布一个事件:SecondChange。观察该类的类订阅了该事件。

    public class Clock
    {

        // 代表小时,分钟,秒的私有变量

        int _hour;

        public int Hour
        {

            get { return _hour; }

            set { _hour = value; }

        }

        private int _minute;

        public int Minute
        {

            get { return _minute; }

            set { _minute = value; }

        }

        private int _second;

        public int Second
        {

            get { return _second; }

            set { _second = value; }

        }

        // 要发布的事件

        public event SecondChangeHandler SecondChange;

        // 触发事件的方法

        protected void OnSecondChange(

        object clock,

        TimeInfoEventArgs timeInformation

        )
        {

            // Check if there are any Subscribers

            if (SecondChange != null)
            {

                // Call the Event

                SecondChange(clock, timeInformation);

            }

        }

        // 让钟(Clock)跑起来,每隔一秒钟触发一次事件

        public void Run()
        {

            for (; ; )
            {

                // 让线程Sleep一秒钟

                Thread.Sleep(1000);

                // 获取当前时间

                System.DateTime dt = System.DateTime.Now;

                // 如果秒钟变化了通知订阅者

                if (dt.Second != _second)
                {

                    // 创造TimeInfoEventArgs类型对象,传给订阅者

                    TimeInfoEventArgs timeInformation =

                    new TimeInfoEventArgs(

                    dt.Hour, dt.Minute, dt.Second);

                    // 通知订阅者

                    OnSecondChange(this, timeInformation);

                }

                // 更新状态信息

                _second = dt.Second;

                _minute = dt.Minute;

                _hour = dt.Hour;

            }

        }

    }

    /* ======================= Event Subscribers =============================== */

    // 一个订阅者。DisplayClock订阅了clock类的事件。它的工作是显示当前时间。

    public class DisplayClock
    {

        // 传入一个clock对象,订阅其SecondChangeHandler事件

        public void Subscribe(Clock theClock)
        {

            theClock.SecondChange +=

            new SecondChangeHandler(TimeHasChanged);

        }

        // 实现了委托匹配类型的方法

        public void TimeHasChanged(

        object theClock, TimeInfoEventArgs ti)
        {

            Console.WriteLine("Current Time: {0}:{1}:{2}",

            ti.hour.ToString(),

            ti.minute.ToString(),

            ti.second.ToString());

        }

    }

    // 第二个订阅者,他的工作是把当前时间写入一个文件

    public class LogClock
    {

        public void Subscribe(Clock theClock)
        {

            theClock.SecondChange +=

            new SecondChangeHandler(WriteLogEntry);

        }

        // 这个方法本来应该是把信息写入一个文件中

        // 这里我们用把信息输出控制台代替

        public void WriteLogEntry(

        object theClock, TimeInfoEventArgs ti)
        {

            Clock a = (Clock)theClock;

            Console.WriteLine("Logging to file: {0}:{1}:{2}",

            a.Hour.ToString(),

            a.Minute.ToString(),

            a.Second.ToString());

        }

    }

    /* ======================= Test Application =============================== */

    // 测试拥有程序

    public class Test
    {

        public static void Main()
        {

            // 创建clock实例

            Clock theClock = new Clock();

            // 创建一个DisplayClock实例,让其订阅上面创建的clock的事件

            DisplayClock dc = new DisplayClock();

            dc.Subscribe(theClock);

            // 创建一个LogClock实例,让其订阅上面创建的clock的事件

            LogClock lc = new LogClock();

            lc.Subscribe(theClock);

            // 让钟跑起来

            theClock.Run();

        }

    }

}

14. 3、小结

(1)、在定义事件的那个类A里面,可以任意的使用事件名,可以触发;在别的类里面,事件名只能出现在 += 或 -= 的左边来指向函数,即只能实例化,不能直接用事件名触发。但是可以通过A类的对象来调用A类中的触发事件的函数。这是唯一触发事件的方式。

(2)、不管是多播委托还是单播委托,在没有特殊处理的情况下,在一个线程的执行过程中去调用委托(委托对象所指向的函数),调用委托的执行是不会新起线程的,这个执行还是在原线程中的,这个对于事件也是一样的。当然,如果是在委托所指向的函数里面去启动一个新的线程那就是另外一回事了。

(3)、事件是针对某一个具体的对象的,一般在该对象的所属类A中写好事件,并且写好触发事件的方法,那么这个类A就是事件的发布者,然后在别的类B里面定义A的对象,并去初始化该对象的事件,让事件指向B类中的某一个具体的方法,B类就是A类事件的订阅者。当通过A类的对象来触发A类的事件的时候(只能A类的对象来触发A类的事件,别的类的对象不能触发A类的事件,只能订阅A类的事件,即实例化A类的事件),作为订阅者的B类会接收A类触发的事件,从而使得订阅函数被执行。一个发布者可以有多个订阅者,当发布者发送事件的时候,所有的订阅者都将接收到事件,从而执行订阅函数,但是即使是有多个订阅者也是单线程。

Leave a Reply

Your email address will not be published. Required fields are marked *