C# 事件与委托(转载)

委托的定义

delegate 是 C# 中的一种类型,它实际上是一个能够持有对某个方法的引用的类。与其它的类不同,delegate 类能够拥有一个方法的签名(signature),并且它"只能持有与它的签名相匹配的方法的引用"。它所实现的功能与 C/C++ 中的函数指针十分相似。它允许你传递类 A 的方法 m() 给另一个类 B 的对象,使得类 B 的对象能够调用这个方法 m。

但与函数指针相比,delegate 有许多函数指针不具备的优点:

首先,函数指针只能指向静态函数,而delegate既可以引用静态函数,又可以引用非静态成员函数。在引用非静态成员函数时,delegate不但保存了对此函数入口指针的引用,而且还保存了调用此函数的类实例的引用。

其次,与函数指针相比,delegate是面向对象、类型安全、可靠的托管(managed)对象。也就是说,runtime 能够保证 delegate 指向一个有效的方法,你无须担心 delegate 会指向无效地址或者越界地址。

对 delegate 对象的操作可以按如下步骤进行:
1. 声明 delegate 对象的格式,让它与你想要传递的方法具有相同的参数和返回值类型。
2. 创建 delegate 对象的实例,并"将你想要传递的函数作为参数传入"。
3. 在要实现异步调用的地方,通过上一步创建的对象来调用方法。

using System;

namespace WindowsFormsApp
{
    public class MyDelegateTest
    {
        // 步骤1,声明delegate对象
        public delegate void MyDelegate(string name);

        // 这是我们欲传递的方法,它与MyDelegate具有相同的参数和返回值类型
        public static void MyDelegateFunc(string name)
        {
            Console.WriteLine("Hello, {0}", name);
        }
        public static void Main()
        {
            // 步骤2,创建delegate对象(实例??)
            MyDelegate md = new MyDelegate(MyDelegateTest.MyDelegateFunc);
            // 步骤3,调用delegate
            md("Tom");

            //输出结果是:Hello, Tom 
        }
    }
}

下面我们来看看,在 C# 中对事件是如何处理的。C# 事件通过委托来定义,实际上事件是一种具有特殊签名的委托,如下:

public delegate void MyEventHandler(object sender, MyEventArgs e);
private event MyEventHandler myevent;
myevent += new MyEventHandler(方法名)

其中的两个参数,sender 代表事件发送者,e 是事件参数类。MyEventArgs 类用来包含与事件相关的数据,所有的事件参数类都必须从 System.EventArgs 类派生。当然,如果你的事件不含参数,那么可以直接用 System.EventArgs 类作为参数。
可以将自定义事件的实现归结为以下几步:
1.定义 delegate 对象类型,它有两个参数,第一个参数是事件发送者对象,第二个参数是事件参数类对象。
2.定义事件参数类,此类应当从 System.EventArgs 类派生。如果事件不带参数,这一步可以省略。
3.定义事件处理方法,它应当与 delegate 对象具有相同的参数和返回值类型。
4.用 event 关键字定义事件对象,它同时也是一个 delegate 对象。
5.用 += 操作符添加事件到事件队列中(-= 操作符能够将事件从队列中删除)。
6.在需要触发事件的地方用调用 delegate 的方式写事件触发方法。一般来说,此方法应为 protected 访问限制,既不能以 public 方式调用,但可以被子类继承。名字是 OnEventName。
7.在适当的地方调用事件触发方法触发事件。

下面是一个简单的例子:

using System;

namespace WindowsFormsApp
{
    public class EventTest
    {
        // 步骤1,定义delegate对象
        public delegate void MyEventHandler(object sender, System.EventArgs e);
        // 步骤2(定义事件参数类)省略
        public class MyEventCls
        {
            // 步骤3,定义事件处理方法,它与delegate对象具有相同的参数和返回值类型
            public void MyEventFunc(object sender, System.EventArgs e)
            {
                Console.WriteLine("My event is ok!");
            }
        }
        // 步骤4,用event关键字定义事件对象
        private event MyEventHandler myevent;
        private MyEventCls myecls;
        public EventTest()
        {
            myecls = new MyEventCls();
            // 步骤5,用+=操作符将事件添加到队列中
            this.myevent += new MyEventHandler(myecls.MyEventFunc);
        }
        // 步骤6,以调用delegate的方式写事件触发函数
        protected void OnMyEvent(System.EventArgs e)
        {
            if (myevent != null)
                myevent(this, e);
        }
        public void RaiseEvent()
        {
            EventArgs e = new EventArgs();
            // 步骤7,触发事件
            OnMyEvent(e);
        }
        public static void Main()
        {
            EventTest et = new EventTest();
            Console.Write("Please input 'a':");
            //string s = Console.ReadLine();
            string s = "a";
            if (s == "a")
            {
                et.RaiseEvent();
            }
            else
            {
                Console.WriteLine("Error");
            }

            //输出结果如下,红色为用户的输入:
            //Please input 'a': a
            //My event is ok!
        }
    }
}

自定义事件

要创建一个事件驱动的程序需要下面的步骤:

1. 声明关于事件的委托

2. 声明事件

3. 编写触发事件的函数

4. 创建事件处理程序

5. 注册事件处理程序

6. 在适当的条件下触发事件

现在我们来编写一个自定义事件的程序。主人养了一条忠实的看门狗,晚上主人睡觉的时候,狗负责看守房子。一旦有小偷进来,狗就发出一个Alarm事件,主人接到Alarm事件后就会采取相应的行动。假设小偷于2009年元旦午夜时分到达。  

using System;

//事件发送者
class Dog
{
    //1.声明关于事件的委托;
    public delegate void AlarmEventHandler(object sender, EventArgs e);

    //2.声明事件;   
    public event AlarmEventHandler Alarm;

    //3.编写引发事件的函数;
    public void OnAlarm()
    {
        if (this.Alarm != null)
        {
            Console.WriteLine("/n狗报警: 有小偷进来了,汪汪~~~~~~~");
            this.Alarm(this, new EventArgs());   //发出警报
        }
    }
}

//事件接收者
class Host
{
    //4.编写事件处理程序
    void HostHandleAlarm(object sender, EventArgs e)
    {
        Console.WriteLine("主  人: 抓住了小偷!");
    }

    //5.注册事件处理程序
    public Host(Dog dog)
    {
        dog.Alarm += new Dog.AlarmEventHandler(HostHandleAlarm);
    }
}

//6.现在来触发事件
class Program
{
    static void Main(string[] args)
    {
        Dog dog = new Dog();
        Host host = new Host(dog);

        //当前时间,从2008年12月31日23:59:50开始计时
        DateTime now = new DateTime(2008, 12, 31, 23, 59, 50);
        DateTime midnight = new DateTime(2009, 1, 1, 0, 0, 0);

        //等待午夜的到来
        Console.WriteLine("时间一秒一秒地流逝... ");
        while (now < midnight)
        {
            Console.WriteLine("当前时间: " + now);
            System.Threading.Thread.Sleep(1000);   //程序暂停一秒
            now = now.AddSeconds(1);                //时间增加一秒
        }

        //午夜零点小偷到达,看门狗引发Alarm事件
        Console.WriteLine("/n月黑风高的午夜: " + now);
        Console.WriteLine("小偷悄悄地摸进了主人的屋内... ");
        dog.OnAlarm();
    }
}

当午夜时分小偷到达时,dog调用dog.OnAlarm()函数,从而触发Alarm事件,于是"系统"找到并执行了注册在Alarm事件中的事件处理程序HostHandleAlarm()。

事件处理委托习惯上以EventHandler结尾,比如AlarmEventHandler。事件Alarm实际上是事件处理委托AlarmEventHandler的一个实例。引发事件的代码常常被编写成一个函数,.NET约定这种函数的名称为“OnEventName”,比如OnAlarm()的函数。在Host类中,我们定义了事件处理程序HostHandleAlarm(),并把它注册到dog.Alarm事件中。 

 

事件处理程序的参数应该和事件委托相同。一般情况下,事件处理程序接受两个参数,一个是事件的发送者sender,一个是事件参数e。事件参数用于在发送者和接收者之间传递信息。

 

.NET提供了100个事件参数类,这些都继承于EventArgs类。一般情况下,使用.NET自带的类足够了,但为了说明原理,我们自定义一个事件参数类。

 试一试:使用事件参数

using System;

namespace WindowsFormsApp
{
    //事件参数
    public class NumberOfThiefEventArgs : EventArgs
    {
        public int numberOfThief;

        //构造函数
        public NumberOfThiefEventArgs(int number)
        {
            numberOfThief = number;
        }
    }
}
namespace WindowsFormsApp
{
    //事件发送者
    class Dog
    {
        //1.声明关于事件的委托;
        public delegate void AlarmEventHandler(object sender, NumberOfThiefEventArgs e);

        //2.声明事件;
        //事件只能在声明了它的类中触发
        //public event AlarmEventHandler Alarm;

        //如果是委托,它还可以在其他类中触发
        public AlarmEventHandler Alarm;

        //3.编写引发事件的函数,注意多了个参数;
        public void OnAlarm(NumberOfThiefEventArgs e)
        {
            if (this.Alarm != null)
            {                
                this.Alarm(this, e);
            }
        }
    }
}
//C#6 新语法,设置全局变量
using static System.Console;

namespace WindowsFormsApp
{
    //事件接收者
    class Host
    {
        //4.编写事件处理程序,参数中包含着numberOfThief信息
        void HostHandleAlarm(object sender, NumberOfThiefEventArgs e)
        {
            if (e.numberOfThief <= 1)
            {
                WriteLine("主人:抓住了小偷!");
            }
            else
            {
                WriteLine("主人:打110报警,我家来了{0}个小偷!", e.numberOfThief);
            }
        }

        //5.注册事件处理程序
        public Host(Dog dog)
        {
            //dog.Alarm += new AlarmEventHandler(HostHandleAlarm);
            dog.Alarm += HostHandleAlarm;
        }
    }
}
using System;

namespace WindowsFormsApp
{
    //6. 现在来触发事件
    static class Program
    {
        static void Main(string[] args)
        {
            Dog dog = new Dog();
            Host host = new Host(dog);

            //当前时间,从2017年4月4日23:59:50开始计时
            DateTime now = new DateTime(2017, 4, 4, 23, 59, 50);
            DateTime midnight = new DateTime(2017, 4, 5, 0, 0, 0);

            //等待午夜的到来
            Console.WriteLine("时间一秒一秒地流逝... ");
            while (now < midnight)
            {
                Console.WriteLine("当前时间: " + now);
                System.Threading.Thread.Sleep(1000);    //程序暂停一秒
                now = now.AddSeconds(1);                //时间增加一秒
            }

            //午夜零点小偷到达,看门狗引发Alarm事件
            Console.WriteLine("月黑风高的午夜: " + now);
            Console.WriteLine("小偷悄悄地摸进了主人的屋内... ");

            //创建事件参数
            NumberOfThiefEventArgs e = new NumberOfThiefEventArgs(3);

            //事件只能在声明了它的类中触发
            dog.OnAlarm(e);

            //如果是委托,它还可以在其他类中触发
            //dog.Alarm(dog, e);            
        }
    }
}

       在修改过的代码中,我们定义了一个名为NumberOfThiefEventArgs的事件参数类,它继承于EventArgs类。在该类中我们声明了一个名为numberOfThief的成员变量,用来记录来了几个小偷。当事件发生时,狗通过事件参数传告诉主人具体信息。 

传递方法的引用

我们先不管这个标题如何的绕口,也不管委托究竟是个什么东西,来看下面这两个最简单的方法,它们不过是在屏幕上输出一句问候的话语:

        public void GreetPeople(string name)
        {
            // 做某些额外的事情,比如初始化之类,此处略
            EnglishGreeting(name);
        }
public void EnglishGreeting(string name) { Console.WriteLine("Morning, " + name); }

暂且不管这两个方法有没有什么实际意义。GreetPeople用于向某人问好,当我们传递代表某人姓名的name参数,比如说“Jimmy”,进去的时候,在这个方法中,将调用EnglishGreeting方法,再次传递name参数,EnglishGreeting则用于向屏幕输出 “Morning, Jimmy”。

现在假设这个程序需要进行全球化,哎呀,不好了,我是中国人,我不明白“Morning”是什么意思,怎么办呢?好吧,我们再加个中文版的问候方法:

public void ChineseGreeting(string name)
{ Console.WriteLine(
"早上好, " + name); }

这时候,GreetPeople也需要改一改了,不然如何判断到底用哪个版本的Greeting问候方法合适呢?在进行这个之前,我们最好再定义一个枚举作为判断的依据:

public enum Language
{ English, Chinese }
public void GreetPeople(string name, Language lang)
{
//做某些额外的事情,比如初始化之类,此处略 swith(lang){ case Language.English: EnglishGreeting(name); break; case Language.Chinese: ChineseGreeting(name); break; } }

OK,尽管这样解决了问题,但我不说大家也很容易想到,这个解决方案的可扩展性很差,如果日后我们需要再添加韩文版、日文版,就不得不反复修改枚举和GreetPeople()方法,以适应新的需求。

在考虑新的解决方案之前,我们先看看 GreetPeople的方法签名:

public void GreetPeople(string name, Language lang)

我们仅看 string name,在这里,string 是参数类型,name 是参数变量,当我们赋给name字符串“jimmy”时,它就代表“jimmy”这个值;当我们赋给它“张子阳”时,它又代表着“张子阳”这个值。然后,我们可以在方法体内对这个name进行其他操作。哎,这简直是废话么,刚学程序就知道了。

如果你再仔细想想,假如GreetPeople()方法可以接受一个参数变量,这个变量可以代表另一个方法,当我们给这个变量赋值 EnglishGreeting的时候,它代表着 EnglsihGreeting() 这个方法;当我们给它赋值ChineseGreeting 的时候,它又代表着ChineseGreeting()方法。我们将这个参数变量命名为 MakeGreeting,那么不是可以如同给name赋值时一样,在调用 GreetPeople()方法的时候,给这个MakeGreeting 参数也赋上值么(ChineseGreeting或者EnglsihGreeting等)?然后,我们在方法体内,也可以像使用别的参数一样使用MakeGreeting。但是,由于MakeGreeting代表着一个方法,它的使用方式应该和它被赋的方法(比如ChineseGreeting)是一样的,比如:

MakeGreeting(name);

好了,有了思路了,我们现在就来改改GreetPeople()方法,那么它应该是这个样子了:

public void GreetPeople(string name, ??? MakeGreeting)
{ MakeGreeting(name); }

注意到 ??? ,这个位置通常放置的应该是参数的类型,但到目前为止,我们仅仅是想到应该有个可以代表方法的参数,并按这个思路去改写GreetPeople方法,现在就出现了一个大问题:这个代表着方法的MakeGreeting参数应该是什么类型的?

NOTE:这里已不再需要枚举了,因为在给MakeGreeting赋值的时候动态地决定使用哪个方法,是ChineseGreeting还是 EnglishGreeting,而在这个两个方法内部,已经对使用“morning”还是“早上好”作了区分。

聪明的你应该已经想到了,现在是委托该出场的时候了,但讲述委托之前,我们再看看MakeGreeting参数所能代表的 ChineseGreeting()和EnglishGreeting()方法的签名:

public void EnglishGreeting(string name)
public void ChineseGreeting(string name)

如同name可以接受String类型的“true”和“1”,但不能接受bool类型的true和int类型的1一样。MakeGreeting的 参数类型定义 应该能够确定 MakeGreeting可以代表的方法种类,再进一步讲,就是MakeGreeting可以代表的方法 的 参数类型和返回类型。

于是,委托出现了:它定义了MakeGreeting参数所能代表的方法的种类,也就是MakeGreeting参数的类型。

NOTE:如果上面这句话比较绕口,我把它翻译成这样:string 定义了name参数所能代表的值的种类,也就是name参数的类型。

本例中委托的定义:

public delegate void GreetingDelegate(string name);

可以与上面EnglishGreeting()方法的签名对比一下,除了加入了delegate关键字以外,其余的是不是完全一样?

现在,让我们再次改动GreetPeople()方法,如下所示:

public void GreetPeople(string name, GreetingDelegate MakeGreeting){
    MakeGreeting(name);
}

如你所见,委托GreetingDelegate出现的位置与 string相同,string是一个类型,那么GreetingDelegate应该也是一个类型,或者叫类(Class)。但是委托的声明方式和类却完全不同,这是怎么一回事?实际上,委托在编译的时候确实会编译成类。因为Delegate是一个类,所以在任何可以声明类的地方都可以声明委托。更多的内容将在下面讲述,现在,请看看这个范例的完整代码:

using System;

namespace Delegate
{
    //定义委托,它定义了可以代表的方法的类型
    public delegate void GreetingDelegate(string name);
    class Program
    {

        private static void EnglishGreeting(string name)
        {
            Console.WriteLine("Morning, " + name);
        }

        private static void ChineseGreeting(string name)
        {
            Console.WriteLine("早上好, " + name);
        }

        //注意此方法,它接受一个GreetingDelegate类型的方法作为参数
        private static void GreetPeople(string name, GreetingDelegate MakeGreeting)
        {
            MakeGreeting(name);
        }

        static void Main(string[] args)
        {
            GreetPeople("Jimmy Zhang", EnglishGreeting);
            GreetPeople("张子阳", ChineseGreeting);
            Console.ReadKey();

            //输出如下:
            //Morning, Jimmy Zhang
            //早上好, 张子阳
        }
    }
}

我们现在对委托做一个总结:

委托是一个类,它定义了方法的类型,使得可以将方法当作另一个方法的参数来进行传递,这种将方法动态地赋给参数的做法,可以避免在程序中大量使用If-Else(Switch)语句,同时使得程序具有更好的可扩展性。 

原文地址:

http://blog.csdn.net/jamestaosh/article/details/4372172

相关文章:

http://blog.csdn.net/cyp403/article/details/1514023

http://www.cnblogs.com/profession/p/4796894.html

http://blog.csdn.net/lulu_jiang/article/details/6451300

http://www.cnblogs.com/linianhui/p/csharp6_using-static.html

posted on 2017-04-13 17:05  wangzhiliang  阅读(267)  评论(0编辑  收藏  举报

导航