zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    链表--判断一个链表是否为回文结构
    矩阵--“之”字形打印矩阵
    二叉树——平衡二叉树,二叉搜索树,完全二叉树
    链表--反转单向和双向链表
    codeforces 490C. Hacking Cypher 解题报告
    codeforces 490B.Queue 解题报告
    BestCoder19 1001.Alexandra and Prime Numbers(hdu 5108) 解题报告
    codeforces 488A. Giga Tower 解题报告
    codeforces 489C.Given Length and Sum of Digits... 解题报告
    codeforces 489B. BerSU Ball 解题报告
  • 原文地址:https://www.cnblogs.com/hellowzl/p/6704816.html
Copyright © 2011-2022 走看看