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

  • 相关阅读:
    Linux 文件排序
    ubuntu18.04 美化桌面
    git clone 加速
    ubunutu下图像编辑器安装
    vue.js实战教程 https://www.jb51.net/Special/978.htm
    原生JS实现多条件筛选
    php结合js实现多条件组合查询
    js前端 多条件筛选查询
    JS 判断字符串是否全部为数字
    GET请求中URL的最大长度限制总结
  • 原文地址:https://www.cnblogs.com/hellowzl/p/6704816.html
Copyright © 2011-2022 走看看