委托和事件

 

委托和事件(转)

一、引言

    委托和事件,这俩个鬼东西呐对老手来说是顺手拈来,但是对新手来说(这里由于我也不懂,那么自动把自己归到新手行列)就困难了,更可气的是你去其他公司参加面试笔试,面试官还总用这种题刁难你。好吧利用这段时间,加上网上一篇非常不错的文章,将这俩个家伙讲清楚到明白。我写的这篇笔记大部分源自这篇文章:http://www.tracefact.net/CSharp-Programming/Delegates-and-Events-in-CSharp.aspxC# 中的委托和事件),如有侵犯版权告诉我处理哈
    本文中,将通过两个范例由浅入深的讲述什么是委托和事件,以及委托事件的由来、dotnet Framework 中的委托和事件、委托和事件对观察者模式的意义、当然以及观察者模式的具体实现等,看了这些对于晕头转向的你,不要怕,我们慢慢展开。

二、委托初探

    先看很简单一段代码如下,这段代码是向某人问好。
        public static void GreetPeople(string name)
        {
            EnglishGreeting(name);
        }
        public static void EnglishGreeting(string name)
        {
            Console.WriteLine("Morning, " + name);
        }

 

很简单,GreetPeople 传入人名 输出:Morning,XXX ;这时有新的需求了,那假如我是中国人,不懂英语,希望你用汉语问好;那代码这块就得重新扩展了以满足更多的需求,是不是很烦躁,没办法,需求永远在变。代码又改为如下形式:
        public static void GreetPeople(Language lang,string name)
        {
            switch (lang)
            {
                case Language.Chinese:
                    ChineseGreeting(name);
                    break;
                case Language.English:
                    EnglishGreeting(name);
                    break;
            }

        }
        public static void EnglishGreeting(string name)
        {
            Console.WriteLine("Morning, " + name);
        }
        public static void ChineseGreeting(string name)
        {
            Console.WriteLine("早上好," + name);
        }
        /// <summary>
        /// 语言枚举
        /// </summary>
        public enum  Language
        {
            English,Chinese
        }

 

 
看到,加了一个 ChineseGreeting() 函数,为了判断调用哪个打招呼函数,有增加了一个枚举。最主要的,调用打招呼函数时逻辑判断恶心的要死,如果我在家韩文的、日文的打招呼,需要不停地修改GreetPeople函数,你会说没什么啊,每增加一门语言,只需要增加一个语言函数,以及修改switch中的判断即可。不然,这违背了面向对象的OCP原则,而却扩展性非常差。在考虑新的解决方案之前?我们先看一下GreetPeople函数的方法签名
publicstaticvoidGreetPeople(Language lang,string name)
string name string 是参数类型,name 是参数值,name 传入 Jack, 它就代表 Jack ; 传入 Mr Zhang 它就代表Mr Zhang (你会说,这不是废话吗,初学者都懂);别着急,那我们试想如果我将一个函数传入呢?这有点类似回调函数一样,完后在 GreetPeople 函数内部调用这个函数,是不是可以实现呢?答案是可以的,类似这样
 public static void GreetPeople(string name, **** MakeGreeting)
 {
     MakeGreeting(name);
 }

 

其中 **** 应该是一种参数类型,具体是啥我也不知道。现在大概我们知道怎么实现了,MakeGreeting 他代表的是一个函数,再看 GreetPeople 中 MakeGreeting 的实现,函数的形式应该是这样的  function(string name) ,现在就该委托出场了,委托的关键字是 delegate ,本例中我们的委托如下定义
   public delegate void GreetDelegate(string name);
 
现在我们再次修改 GreetPeople() 函数
        public static void GreetPeople(string name,GreetDelegate makeGreeting)
        {
            makeGreeting(name);
        }

 

如你所见 委托 GreetDelegate出现的位置 就是 string 出现的位置,string 是一种类型,那么GreetDelegate也是一种类型了,或者叫做类,但是委托的声明方式与类完全不同,这是怎么一回事呢?实际上在编译的时候委托确实会被编译成类。因为 Delegate 就是一个类,所以任何可以声明类的地方都可以声明委托;下面是该实例的完整代码
       public static void Main(string[] args)
        {
            GreetPeople("Zhang",EnglishGreeting);
            GreetPeople("Mr zhang", ChineseGreeting);
        }
        public static void GreetPeople(string name,GreetDelegate makeGreeting)
        {
            makeGreeting(name);
        }
        public static void EnglishGreeting(string name)
        {
            Console.WriteLine("Morning, " + name);
        }
        public static void ChineseGreeting(string name)
        {
            Console.WriteLine("早上好," + name);
        }
        public delegate void GreetDelegate(string name);

 

现在我们对委托做一个总结:
        委托是一个类,它定义了方法的类型,可以将方法当做另一个方法的参数进行传递,这种将方法动态的赋给参数的做法,可以避免在程序中大量使用 If-else(switch)语句,同时使程序具有更好的扩展性。

三、委托深究

1. 既然委托和类很像,那么我们是不是可以这样玩呢?
            GreetDelegate delegate1 = EnglishGreeting;
            GreetDelegate delegate2 = ChineseGreeting;
            GreetPeople("Zhang", delegate1);
            GreetPeople("Mr zhang", delegate2);

 

2. 委托还可以类似string 特性一样这样玩,会依次执行
            GreetDelegate myDelegate = EnglishGreeting;
            myDelegate += ChineseGreeting;
            GreetPeople("Zhang", myDelegate);

 

3. 更奇妙的我们还可以绕过GreetPeople这样玩
            GreetDelegate myDelegate = EnglishGreeting;
            myDelegate += ChineseGreeting;
            myDelegate("Zhang");

 

 我们看到第一次给myDelegate赋值时用“=”号,第二次用“+=”,如果我们第一次就用“+=”会报 “使用了未赋值的的局部变量”  ,这时我们想到 GreetDelegate  和一个类的申明很像,那我们可以这样声明来确保使用 “+=” 不报错。
 
            GreetDelegate myDelegate = new GreetDelegate(EnglishGreeting);
            myDelegate += ChineseGreeting;

 

那你会说我也是不是可以这样玩呢?
            GreetDelegate myDelegate=new GreetDelegate();
            myDelegate += EnglishGreeting;
            myDelegate += ChineseGreeting;
嗯,这样写看似正确,但实际上是不行的,会报:“GreetDelegate方法没有采取0个的构造函数”,当然你说那我在声明委托像声明类一样,创建0个参数的构造函数不就可以嘛,先别急,我们把基础知识过完。
委托可以绑定一个或多个方法,使用“+=”符号,我们也会想到解绑方法,如下
            GreetDelegate myDelegate = new GreetDelegate(EnglishGreeting);
            myDelegate += ChineseGreeting;
            myDelegate("zhang");

            myDelegate -= EnglishGreeting;
            myDelegate("张san");

 

现在我们再对委托做一个总结:
        使用委托可以将多个方法绑定到同一个委托变量上,当调用此变量时,可以依次调用所有绑定的方法。

四、引出事件

    我们继续思考上面的程序,在实际操作中这三个方法不会都在 Program 类中,这样做我们只是为了理解方便;通常情况下 GreetPeople 在一个类中,ChineseGreeting 和 EnglishGreeting 在一个类中,现在你已经对委托有一定的了解了,那我们就改动一下,引出事件这个家伙吧。
namespace ConsoleApplication1
{
    public delegate void GreetDelegate(string name);
    class Programe
    {
        public static void Main(string[] args)
        {
        }
        public static void EnglishGreeting(string name)
        {
            Console.WriteLine("Morning, " + name);
        }
        public static void ChineseGreeting(string name)
        {
            Console.WriteLine("早上好," + name);
        }
    }

    public class GreetingManager
    {
        public void GreetPeople(string name, GreetDelegate makeGreeting)
        {
            makeGreeting(name);
        }
    }
}

 

现在如果想实现上面的输出,Main 函数里需要这样写:
        public static void Main(string[] args)
        {
            GreetingManager gm = new GreetingManager();
            gm.GreetPeople("zhang",EnglishGreeting);
            gm.GreetPeople("张san",ChineseGreeting);
        }

 

接下来,我们再将上一节学到的内容,将方法绑定到委托上
            GreetingManager gm = new GreetingManager();
            GreetDelegate myDelegate;
            myDelegate = EnglishGreeting;
            myDelegate += ChineseGreeting;

            gm.GreetPeople("zhang", myDelegate);

 

到了这里我们不禁想到:面向对象设计,讲究的是对象的封装,既然可以声明委托类型的变量(例子中的myDelegate),我们为何不将该对象封装到GreetingManager类中呢,在这个类的客户端使用不是更方便么,像这样:
    public class GreetingManager
    {
        //在GreetingManager类中声明myDelegate对象
        public GreetDelegate myDelegate;
        public void GreetPeople(string name, GreetDelegate makeGreeting)
        {
            makeGreeting(name);
        }
    }

 

现在我们可以这样使用委托对象:
            GreetingManager gm = new GreetingManager();
            gm.myDelegate = EnglishGreeting;
            gm.myDelegate += ChineseGreeting;

            gm.GreetPeople("zhang", gm.myDelegate);
尽管这样做没有任何问题,但我们发现这条语句很奇怪。在调用gm.GreetPeople方法后,又传递了gmmyDelegate字段
           gm.GreetPeople("zhang", gm.myDelegate);
既然都属于GreetingManager类,那这些完全可以在该类内部实现,如下:
    public class GreetingManager
    {
        //在GreetingManager类中声明myDelegate对象
        public GreetDelegate MyDelegate;
        public void GreetPeople(string name)
        {
            if (MyDelegate!=null)
            {
                MyDelegate(name);
            }
        }
    }

 

客户端调用的时候,也更简洁了一些,如下:
            GreetingManager gm = new GreetingManager();
            gm.MyDelegate = EnglishGreeting;
            gm.MyDelegate += ChineseGreeting;

            gm.GreetPeople("zhang");

 

尽管这样已经达到了我们想要的效果,但是还是存在这问题:
在这里,MyDelegate 和我们平时用的string类型的变量没有什么区别,而我们知道不是所有字段都应该声明成public 的,应该是该public的时候public,该private的时候private。
我们先看看把 MyDelegate 声明成private会怎样,结果:这就搞笑了,因为声明委托的目的就是要让它暴露在类的客户端进行注册,你把它声明为private,客户端根本就看不见它,那还注册个鬼啊,再看看声明成public会怎样?结果是:在客户端可以随意的给它赋值等操作,这破换了面向对象的封装性
现在想一想,如果 MyDelegate 是string你会怎样?是不是想着把它使用属性对字段进行封装呢?答案是正确的。
于是呢 Event 出场了,它封装了委托类型的变量,使得:在类内部,不管你声明它是 public 还是 protected ,它总是 private的。在类外部,注册 “+=” 和注销 “ -=” 的访问限定符与你在声明时使用的访问符相同
我们修改GreetingManager类如下:
 public class GreetingManager
    {
        //这次我们在这里声明一个事件
        public event  GreetDelegate MakeGreet;
        public void GreetPeople(string name)
        {
            if (MyDelegate!=null)
            {
                MyDelegate(name);
            }
        }
    }

 

这里我们可以看到,声明事件指示在声明委托中多了一个 event 关键字而已,再结合上面的讲解,你应该明白:声明事件不过是声明了一个进行了封装的委托类型的对象而。这看上去没什么大不同,但是使用的时候报错
            GreetingManager gm = new GreetingManager();
            gm.MakeGreet = EnglishGreeting;    //编译报错
            gm.MakeGreet += ChineseGreeting; 

            gm.GreetPeople("zhang");

 

报错:事件只能出现在+=或-=的左边...

五、深度观察

我们可以用发编译工具对  MyDelegate 做一番探究
我们再看一下MyDelegate 所产生的代码
private GreetingDelegate MakeGreet; //对事件的声明 实际是 声明一个私有的委托变量
 
[MethodImpl(MethodImplOptions.Synchronized)]
public void add_MakeGreet(GreetingDelegate value){
    this.MakeGreet = (GreetingDelegate) Delegate.Combine(this.MakeGreet, value);
}

[MethodImpl(MethodImplOptions.Synchronized)]
public void remove_MakeGreet(GreetingDelegate value){
    this.MakeGreet = (GreetingDelegate) Delegate.Remove(this.MakeGreet, value);
}

 

再看上面的代码就很明确了,MyDelegate事件确实是一个 GreetDelegate 的委托,只不过不管是声明了public 还是protected,它总是被声明为private。另外它还有两个方法 add_MakeGreet和remove_MakeGreet,这两个方法分别用于注册委托类型的方法和取消注册。add_MakeGreet 对应着 “+=”,而remove_MakeGreet对应着 “-=”,而这俩个方法的访问限制取决于声明事件时的访问限制。
 

六、委托、事件与Observer设计模式

    先举个例子:假设我们有个高档热水器,我们给它通上电,当水温超过95°时:1. 扬声器会发出语音告诉你,水的温度。2. 液晶屏也会改变水温的温度显示,来提示水快要烧开了。现在我们用程序来模拟这一过程:一个热水器类(Heater),里面有代表水温的字段 temperature ;还有必不可少的给谁加温的方法 BoilWater(),一个发出语音警报的方法 MakeAlert(),一个显示水温的方法,ShowMsg().。代码实现:
class Programe
    {
        public static void Main(string[] args)
        {
            Heater heater = new Heater();
            heater.BoilWater();
        }
    }
    public class Heater
    {
        private int temperature;//水温

        public void BoilWater()
        {
            for (var index = 0; index <= 100; index++)
            {
                temperature = index;
                MakeAlert(temperature);
                ShowMsg(temperature);
                Thread.Sleep(500);
            }
        }
        private void MakeAlert(int temperature)
        {
            if (temperature>=95)
            {
                Console.WriteLine("注意了,水马上要开了");
            }
        }
        private void ShowMsg(int temperature)
        {
            Console.WriteLine("现在水温:" + temperature + "度。");
        }
    }

 

运行发现,可以完成我们所描述的工作,但是却并不够好,假如热水器更复杂一些,由这三部分组成:热水器、警报器、显示器;他们来自不同的厂商并进行了组装。那么热水器仅仅负责烧水,其他不是他分内的事他不干,警报器只负责超过设定的温度后报警,显示器只负责显示温度。这时候我们上面的例子就要修改为:
class Programe
    {
        public static void Main(string[] args)
        {
            Heater heater = new Heater();
            heater.BoilWater();
        }
    }
    public class Heater
    {
        private int temperature;//水温
        public void BoilWater()
        {
            for (var index = 0; index <= 100; index++)
            {
                temperature = index;
                Thread.Sleep(500);
            }
        }
    }

    public class Alerm
    {
        public void MakeAlert(int temperature)
        {
            if (temperature >= 95)
            {
                Console.WriteLine("注意了,水马上要开了");
            }
        }
    }
    public class Display
    {
        public void ShowMsg(int temperature)
        {
            Console.WriteLine("现在水温:" + temperature + "度。");
        }
    }

 

这时候就有问题了,如何在水快要开的时候调用警报器和显示器进行显示温度和报警呢,在继续进行之前我们有必要先了解一下 Observer 模式,Observer 模式中包含两类对象:
1. Subject :被监视对象,它往往包含着其他对象感兴趣的内容。在本例中,报警器和显示器对加热器中的温度字段感兴趣。
2. Observer:监视者,它监视 Subject ,当Subject中的某件事发生的时候,会告知Observer,而Observer 采取相应的行动。在本例中 Observer 是警报器和显示器,Subject是热水器。
 
在本例中,事情的发生顺序是这样的:
1. 报警器和显示器告诉热水器,对它的温度感兴趣(注册动作);
2. 热水器知道后保留报警器和显示器的引用。
3. 热水器开启烧水这一动作,当水温到达95°时自动调用警报器的MakeAlert()方法、显示器的ShowMsg()方法
 
在GOF中是这样描述的:Observer 设计模式 —— 是定义对象间的一种一对多的依赖关系,以便当一个对象的状态改变时,其他依赖他的对象会被自动告知并采取行动。Observer模式也是一种松耦合的设计模式
下面直接给出采用 Observer 模式设计出这以功能的代码:
 class Programe
    {
        public static void Main(string[] args)
        {
            Heater heater = new Heater();
            Alerm alerm = new Alerm();

            heater.BoilEvent += alerm.MakeAlert; //注册方法
            heater.BoilEvent += (new Alerm()).MakeAlert; //给匿名对象注册方法
            heater.BoilEvent += Display.ShowMsg; //给静态方法注册

            heater.BoilWater();
        }
    }
    public class Heater
    {
        private int temperature;//水温

        public delegate void BoilHandler(int temp);  //声明委托
        public event BoilHandler BoilEvent;  //声明事件
        public void BoilWater()
        {
            for (var index = 0; index <= 100; index++)
            {
                temperature = index;
                Thread.Sleep(500);
                if (index > 95)
                {
                    if (BoilEvent != null) //如果有对象注册
                    {
                        BoilEvent(temperature); //调用所有注册对象的方法
                    }
                }
                else
                {
                    Console.WriteLine("当前温度:{0}度", index);
                }
            }
        }
    }
    public class Alerm
    {
        public void MakeAlert(int temperature)
        {
            if (temperature >= 95)
            {
                Console.WriteLine("Alerm : 嘀嘀嘀,水已经{0}度了", temperature);
            }
        }
    }
    public class Display
    {
        public static void ShowMsg(int temperature)
        {
            Console.WriteLine("Display:水烧开了,当前温度::{0}度。", temperature);
        }
    }

 

七、.Net Framework 中的委托和事件

    这一节我也不太明白,先记下来后续能力达到一定程度再理清。
    尽管我们上面很好的用委托和事件完成了工作,但我们会发现上面的委托事件模型与 .net framework 中的不太一样呢,为什么会有EventAgrs参数?
    在回答上面的问题之前,我们先搞懂 .Net Framework 的编码规范
  •  委托类型的名称都应该以 EventHandler结束
  • 委托原型定义:有一个void返回值,并接受两个输入参数:一个Object类型,一个EventAgrs类型的参数(或者继承自EventAgrs)
  • 事件的命名为委托去掉EventHandler之后剩余部分
  • 继承自EventArgs的类型的应该以EventArgs结尾
再做一下说明:
    1. 委托声明原型中的Object类型的参数代表了Subject,也就是监视对象,在本例中是Heater(热水器)。回调函数(比如Alarm中的MakeAlert)可以通过它访问触发事件的对象(Heater).
    2. EventArgs 对象包含了Observer 所感兴趣的数据,本例中是temperature
 
上面仅仅是编码规范而已,这样使得程序具有更大的灵活性。比如说,比如说我们不光想获得热水器的温度,还想在Observer端(警报器或显示器)的方法中获得生产日期、型号、价格等等,那么委托和方法的声明将会变得很麻烦,而如果我们将热水器的应用传递给警报器方法,就可以在方法中直接访问热水器了。
 
根据以上规范,我们将代码修改为:
class Programe
    {
        public static void Main(string[] args)
        {
            Heater heater = new Heater();
            Alerm alerm = new Alerm();

            heater.Boiled += alerm.MakeAlert; //注册方法
            heater.Boiled += (new Alerm()).MakeAlert; //给匿名对象注册方法
            heater.Boiled += Display.ShowMsg; //给静态方法注册

            heater.BoilWater();
        }
    }
    public class Heater
    {
        private int temperature;//水温
        public string type = "小天鹅 0047";  //产品编号
        public string area = "Zhejiang China";  //出产地

        //定义BoiledEventArgs类,传递给Observer所感兴趣的信息
        public class BoilEventArgs : EventArgs
        {
            public readonly int teperature;

            public BoilEventArgs(int temperature)
            {
                this.teperature = temperature;
            }
        }

        public delegate void BoilEventHandler(Object sender,BoilEventArgs e);  //声明委托
        public event BoilEventHandler Boiled;  //声明事件

        //可以供继承自Heater的类重写,以便继承类拒绝其他对象对它的监视
        protected virtual void OnBoiled(BoilEventArgs e)
        {
            if (Boiled != null)//如果有对象注册
            {
                Boiled(this, e);//调用所有注册的方法
            }
        }

        public void BoilWater()
        {
            for (var index = 0; index <= 100; index++)
            {
                temperature = index;
                Thread.Sleep(500);
                if (index > 95)
                {
                    //建立BoiledEventArgs 对象。
                    BoilEventArgs e = new BoilEventArgs(temperature);
                    OnBoiled(e);
                }
                else
                {
                    Console.WriteLine("当前温度:{0}度", index);
                }
            }
        }
    }
    public class Alerm
    {
        public void MakeAlert(Object sender,Heater.BoilEventArgs e)
        {
            Heater heater = (Heater)sender;  //这里是不是很熟悉?

            //访问 sender 中的共有字段
            Console.WriteLine("Alerm:{0}-{1}:",heater.area,heater.type);
            Console.WriteLine("Alerm : 嘀嘀嘀,水已经{0}度了", e.teperature);

        }
    }
    public class Display
    {
        public static void ShowMsg(Object sender, Heater.BoilEventArgs e)
        {
            Heater heater = (Heater)sender;

            Console.WriteLine("Display:{0}-{1}:", heater.area, heater.type);
            Console.WriteLine("Display : 水烧开了,当前温度{0}度。", e.teperature);
        }
    }

 

八、总结
    洋洋洒洒,用了一天半的时间跟着文章学习了一把事件和委托;文中首先通过一个GreetingPeople的小程序向大家介绍了委托的概念及用途,随后又引出事件。在第二个稍微复杂的例子中,又介绍了Observer模式,并通过实现这个范例完成了该模式,随后讲解了.Net Framework 中委托、事件的实现方式,希望通过这些暂时让我对委托和事件有一个初步了解。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
posted @ 2019-04-30 16:40  NCat  阅读(186)  评论(0)    收藏  举报