事件引入和本质
前言
继上一篇委托后,我们继续来探讨事件,因为委托和事件有着不可分割的关系。通过本文,相信你会对事件有更深刻的认识和理解,不信,你看!
概念
用event 关键字使您可以声明事件。 事件是类在相关事情发生时发出通知的方法。【简述】事件就是类在发生其关注的事情的时候用来提供通知的一种方式,要理解事件必须要先知道一下两个角色:
发行者(Publisher)
事件发行者,也称为发送者(sender),说白了就是一个对象,这个对象会自身维护本身的状态信息。当自身状态信息发生变动时,就触发一个事件,并通知所有事件的订阅者
订阅者(Subscriber)
对事件感兴趣的对象,也成为接受者(recevier),可以注册你感兴趣的事件,通常会其提供一个事件处理程序,在事件发行者触发一个事件后,会自动触发其代码内容。
最经典的例子莫过于用户向报社订阅杂志,下面就用一张图来说明事件的流程。
报社有各种各样的杂志,当用户想要查看所需杂志时,则必须首先得 订阅 该杂志,当报社 发行 了该杂志时,则订阅了杂志的用户就能收到该杂志!所以这个例子中事件就是报社发行的杂志事件,同时将该杂志分发给已经订阅该杂志的用户手中,从而形成了一个完备的事件机制。下面我们就此例用代码来进行实现。
1 public class Publisher /*发行者*/ 2 { 3 public delegate void Publish(); /*事件代理*/ 4 public event Publish OnPublish; /*事件*/ 5 public void Issue() /*触发事件方法*/ 6 { 7 if (OnPublish != null) 8 { 9 Console.WriteLine("发行刊物"); 10 OnPublish(); 11 } 12 } 13 } 14 public class Subscriber /*订阅者*/ 15 { 16 public void Recieve() /*订阅者事件处理程序*/ 17 { 18 Console.WriteLine("接受刊物"); 19 } 20 } 21 class Program 22 { 23 static void Main(string[] args) 24 { 25 Publisher pub = new Publisher(); 26 Subscriber user = new Subscriber(); 27 pub.OnPublish += new Publisher.Publish(user.Recieve); /*向事件发行者订阅一个事件*/ 28 pub.Issue(); 29 Console.ReadKey(); 30 } 31 }
public delegate void Publish(); 事件的代理。 Issue() 触发事件的方法。 Recieve() 订阅者事件处理程序。控制台输出入下:
通过这一个例子想必我们对事件机制有了初步的认识。接下来为了更真实的理解事件机制,我们假设有如下情景:由于该报社混的不错,在一小段时间内取得了不错的业绩,开始发行两种杂志,一种是博客园杂志,另一种是博问杂志,鉴于此现将代码进行改写如下:
1 public class Publisher 2 { 3 public delegate void PubCnblogs(string magezineName); 4 public delegate void PubCnblogsQuestion(string magezineName); 5 public event PubCnblogs OnPubCnblogs; 6 public event PubCnblogsQuestion OnPubCnblogsQuestion; 7 public void IssueCnblogs() 8 { 9 if (OnPubCnblogs != null) 10 { 11 Console.WriteLine("报社发行博客园杂志"); 12 OnPubCnblogs("博客园"); 13 } 14 } 15 public void IssueCnblogsQuestion() 16 { 17 if (OnPubCnblogsQuestion != null) 18 { 19 Console.WriteLine("报社发行博问杂志"); 20 OnPubCnblogsQuestion("博文"); 21 } 22 } 23 } 24 public class Subscriber 25 { 26 public string Name { get; set; } 27 28 public Subscriber(string name) 29 { 30 this.Name = name; 31 } 32 public void Recieve(string magezineName) 33 { 34 Console.WriteLine(this.Name + "接受" + magezineName + "杂志"); 35 } 36 } 37 class Program 38 { 39 static void Main(string[] args) 40 { 41 Publisher pub = new Publisher(); 42 Subscriber xh = new Subscriber("小红"); 43 pub.OnPubCnblogs += new Publisher.PubCnblogs(xh.Recieve); 44 45 46 Subscriber xm = new Subscriber("小明"); 47 pub.OnPubCnblogs += new Publisher.PubCnblogs(xm.Recieve); 48 pub.OnPubCnblogsQuestion += new Publisher.PubCnblogsQuestion(xm.Recieve); 49 50 pub.IssueCnblogs(); 51 pub.IssueCnblogsQuestion(); 52 53 Console.WriteLine(); 54 Console.WriteLine("过了一段时间"); 55 pub.OnPubCnblogsQuestion -= new Publisher.PubCnblogsQuestion(xm.Recieve); 56 pub.IssueCnblogs(); 57 pub.IssueCnblogsQuestion(); 58 Console.ReadKey(); 59 } 60 61 62 }
看上述代码报社发行了博客园杂志和博问杂志,小红只订阅博客园杂志,小明订阅了博客园杂志和博问杂志,并通过 IssueCnblogs 和 IssueCnblogsQuestion 方法来进行触发发行这两种杂志,经过一段时间后,小明取消订阅博问杂志,最后小红和小明收到的都是订阅的博客园杂志。打印如下:
综上所述我们得出定义一个完整事件有四个步骤:
在事件发行者中定义事件
在事件发行者中触发事件
在事件订阅者中定于事件处理程序
事件订阅者向事件发行者订阅事件
事件设计准则
事件的命名准则应使用PascalCasing的命名方式。
声明delegate时,使用void类型作为返回值,EventName事件的事件委托是EventNameHandler,事件接受传入两个参数,一律命名为sender和e。
定义一个提供事件数据的类,对类以EventNameEventArgs进行命名,从System.EventArgs派生该类,然后添加所有事件的特定成员。
如果事件不需要传递任何参数,也要声明两个参数,e参数可以直接使用系统提供的System.EventArgs类,如果需要传递数据,则要从System.EventArgs继承一个类,并把数据放在里面。如:
public delegate void EventNameHandler (object sender, EventArgs e) public event EventNameHandler EventName;
在触发事件的类中提供一个受保护的方法。以EventName进行命名,在该方法中触发该事件。例如:
protected virtual void OnEventName (EventArgs e) { if (EventName != null) { EventName (this,e); } }
下面就此规范对报社发行杂志和订阅者订阅杂志进行改造,如下:
1 public class PubEventArgs : EventArgs /*触发事件的类PubEventArgs*/ 2 { 3 private readonly string magezine_name; 4 private readonly DateTime magezine_time; 5 public PubEventArgs(string magezineName, DateTime magezineTime) 6 { 7 magezine_name = magezineName; 8 magezine_time = magezineTime; 9 } 10 public string magezineName 11 { 12 get { return magezine_name; } 13 } 14 15 public DateTime magezineTime 16 { 17 get { return magezine_time; } 18 } 19 } 20 public class Publisher /*发行者(报社即事件的发行者)*/ 21 { 22 public delegate void PubCnblogsEventHandler(object sender, PubEventArgs e); /*发行博客园杂志的委托事件代理*/ 23 public delegate void PubCnblogsQuestionEventHandler(object sender, PubEventArgs e); /*发行博问杂志的委托事件代理*/ 24 public event PubCnblogsEventHandler PubCnblogs; /*发行博客园杂志事件*/ 25 public event PubCnblogsQuestionEventHandler PubCnblogsQuestion; /*发行博问杂志事件*/ 26 27 public virtual void OnPubCnblogs(PubEventArgs e) /*提供一个触发发行博客园杂志事件的受保护的方法*/ 28 { 29 PubCnblogsEventHandler cnblogs = PubCnblogs; 30 if (cnblogs != null) 31 { 32 cnblogs(this, e); 33 } 34 } 35 public virtual void OnPubCnblogsQuestion(PubEventArgs e) /*提供一个触发发行博问杂志事件的受保护的方法*/ 36 { 37 PubCnblogsQuestionEventHandler question = PubCnblogsQuestion; 38 if (question != null) 39 { 40 question(this, e); 41 } 42 } 43 public void IssueCnblogs(string magezine_name, DateTime magezine_time) /*触发发行博客园杂志的方法*/ 44 { 45 Console.WriteLine("发行" + magezine_name); 46 OnPubCnblogs(new PubEventArgs(magezine_name, magezine_time)); 47 } 48 public void IssueCnblogsQuestion(string magezine_name, DateTime magezine_time) /*触发发行博问杂志的方法*/ 49 { 50 Console.WriteLine("发行" + magezine_name); 51 OnPubCnblogsQuestion(new PubEventArgs(magezine_name, magezine_time)); 52 } 53 } 54 public class Subscriber /*订阅者(用户即事件的接受者)*/ 55 { 56 public string Name { get; set; } 57 58 public Subscriber(string name) 59 { 60 this.Name = name; 61 } 62 public void Recieve(object sender, PubEventArgs e) /*订阅者订阅事件处理程序*/ 63 { 64 Console.WriteLine(e.magezineTime + " " + Name + "接受到" + e.magezineName); 65 } 66 } 67 class Program 68 { 69 static void Main(string[] args) 70 { 71 Publisher pub = new Publisher(); /*实例化发行者*/ 72 Subscriber xh = new Subscriber("小红"); /*实例化订阅者小红*/ 73 pub.PubCnblogs += new Publisher.PubCnblogsEventHandler(xh.Recieve); /*小红订阅博客园杂志*/ 74 75 76 Subscriber xm = new Subscriber("小明"); /*实例化订阅者小明*/ 77 pub.PubCnblogs += new Publisher.PubCnblogsEventHandler(xm.Recieve); /*小明订阅博客园杂志*/ 78 pub.PubCnblogsQuestion += new Publisher.PubCnblogsQuestionEventHandler(xm.Recieve); /*小明订阅博问杂志*/ 79 80 pub.IssueCnblogs("博客园杂志", DateTime.Now); /*报社发行博客园杂志(触发博客园杂志事件)*/ 81 pub.IssueCnblogsQuestion("博问杂志", DateTime.Now); /*报社发行博问杂志(触发博问杂志事件)*/ 82 83 Console.WriteLine(); 84 85 Console.WriteLine("过了一段时间"); 86 87 pub.PubCnblogsQuestion += new Publisher.PubCnblogsQuestionEventHandler(xh.Recieve); /*小红觉得博客园杂志不错,继续订阅博问杂志*/ 88 pub.PubCnblogsQuestion -= new Publisher.PubCnblogsQuestionEventHandler(xm.Recieve); /*小明觉得有点太多,看不过来,于是取消订阅博问杂志*/ 89 pub.IssueCnblogs("博客园杂志", DateTime.Now); /*报社发行博客园杂志(触发博客园杂志事件)*/ 90 pub.IssueCnblogsQuestion("博问杂志", DateTime.Now); /*报社发行博问杂志(触发博问杂志事件)*/ 91 Console.ReadKey(); 92 } 93 94 95 }
根据上述代码打印出:
通过上述学习我们知道了事件和基本用法以及相关命名规范,下面我们一起来探讨事件本质。
事件本质
通过上述我们写的 Publisher 发行者类,我们通过反编译工具查看其IL代码入下:
我们单拿发行者中的博问委托来看即可,我们上述在代码中定义的委托变量是 public event PubCnblogsQuestionEventHandler PubCnblogsQuestion; 但此时我们发现变量的访问权限变成了 private 私有的,同时该私有委托变量中添加了两个方法 add 和 remove ,所以此时我们就得出这样两个结论,通过添加 event 关键字:(1)自动将委托变量变成了私有的(2)同时生成了add()和remove()两个方法,也就说明事件中+=和-=是通过add()和remove()方法来实现的。
鉴于此我们看看add()方法到底是什么东西?
add()内部实现最终是调用了委托的最终的父类的Delegate中的Combine方法来实现的,同时我们也能看到 this.PubCnblogsQuestion ,通过上述我们知道用event关键字声明的委托变量是私有的,那这个调用的PubCnblogsQuestion变量是哪来的呢?不难看出通过event关键字声明中其实就自动生成了委托变量同名的变量。还不信的话,请看下图中的操作,会让你深信不疑。
当实例化发行者类后再对其委托变量PubCnblogsQuestion进行赋值再编译生成会通不过,上面也有说,此时你操作的是event关键字会自动生成与声明的委托变量同名的一个变量只能对其进行+=或者-=操作,而你声明的委托变量是私有的,你是无法访问的。通过这也验证了这一观点。同理我们查看其remove()方法的实现,想必你也能猜到,肯定也是操作委托中的最终父类Delegate中的Remove()方法对其进行-=操作。眼见为实:
总结
事件的本质即用event关键字声明的本质
用event关键字修饰委托变量时,会将其委托变量私有化,同时自动生成一个与该委托变量同名的变量
用event关键字修饰委托变量时,此时会自动生成add()和remove()两个方法即事件中的+=或者-=都是通过这两个方法来实现,同时最终的实现都是操作该私有化的委托对象的最终父类Delegate中的Combine()和Reomve()方法
作用(好处)
委托依赖事件,何以见得呢?那么 想想如果没有事件会怎样,想想当在实际项目中,上一个程序员用到了委托,当该程序员卷铺盖走人,等到下一个程序员来时不知道里面到底实现了什么或者说对其写的委托实现进行了remove,那可怎么办!如果有了事件的话,你根本无法去操作委托对象,只能通过+=和-=来操作,这样就避免了委托的滥用!