委托和事件
委托和事件(转)
一、引言
委托和事件,这俩个鬼东西呐对老手来说是顺手拈来,但是对新手来说(这里由于我也不懂,那么自动把自己归到新手行列)就困难了
,更可气的是你去其他公司参加面试笔试,面试官还总用这种题刁难你
。好吧利用这段时间,加上网上一篇非常不错的文章,将这俩个家伙讲清楚到明白
。我写的这篇笔记大部分源自这篇文章:http://www.tracefact.net/CSharp-Programming/Delegates-and-Events-in-CSharp.aspx(C# 中的委托和事件),如有侵犯版权告诉我处理哈
。
,更可气的是你去其他公司参加面试笔试,面试官还总用这种题刁难你
。好吧利用这段时间,加上网上一篇非常不错的文章,将这俩个家伙讲清楚到明白
。我写的这篇笔记大部分源自这篇文章:http://www.tracefact.net/CSharp-Programming/Delegates-and-Events-in-CSharp.aspx(C# 中的委托和事件),如有侵犯版权告诉我处理哈
。 本文中,将通过两个范例由浅入深的讲述什么是委托和事件,以及委托事件的由来、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方法后,又传递了gm的myDelegate字段
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 中委托、事件的实现方式,希望通过这些暂时让我对委托和事件有一个初步了解。
真正的大师永远怀着一颗学徒的心。

浙公网安备 33010602011771号