前面简要介绍了委托的基本知识,包括委托的概念、匿名方法、Lambda表达式等,现在讲讲与委托相关的另一个概念:事件。
事件由委托定义,因为事件的触发方(或者说发布方)并不知道事件的订阅方会用什么样的函数名称,这个函数名称由订阅方自己决定。假如不这样做,那么事件的订阅方必须公开一个专门用于处理事件的函数给事件触发方,由触发方在事件触发的时候调用这个函数。这样一来,触发方必须知道订阅方的细节,才能有效地触发事件,显然这是不合理的,触发方与订阅方耦合性太大了,不具备通用性。
事实上,事件的触发方只需要确定好事件处理函数的签名即可。也就是说,触发方只需要定义在事件发生时需要传递的参数,而在订阅方,只需要根据这个签名定义一个处理函数,然后将该函数“绑定”到事件列表,就可以通过签名中的参数,对事件做相应的处理。定义函数签名非常简单,就是使用委托。下面我们来简单看一个例子。这个例子模拟一个服务器程序,它有Start和Stop两个操作,分别表示启动和停止服务。在成功启动以及成功停止时,都会触发一个“成功”的事件,并公布事件发生的确切时间。
一、定义函数签名(委托)
其实对于我们的例子,事件处理函数的签名中只需要一个参数,就是事件发生的确切时间。因此在定义委托的时候,只需要定义一个时间(DateTime)类型的参数即可。为了能够让我们的程序看上去更加标准,并且为了后面描述的方便,我们还是将这个参数封装在一个类里,并且该类继承于System.EventArgs类。
- public class ServerEventArgs : System.EventArgs
- {
- #region Public Properties
- /// <summary>
- /// 读取或设置服务器事件发生的时间
- /// </summary>
- public DateTime FireDateTime { get; set; }
- #endregion
- #region Constructors
- /// <summary>
- /// 默认构造函数,使用当前时间作为服务器事件
- /// 发生的时间
- /// </summary>
- public ServerEventArgs()
- {
- this.FireDateTime = DateTime.Now;
- }
- /// <summary>
- /// 使用给定的事件作为服务器事件发生的时间并
- /// 对参数对象进行初始化
- /// </summary>
- /// <param name="fireDateTime"></param>
- public ServerEventArgs(DateTime fireDateTime)
- {
- this.FireDateTime = fireDateTime;
- }
- #endregion
- }
现在来定义这个委托:
- public delegate void ServerEventHandler(object sender, ServerEventArgs e);
委托的第一个参数表示事件将由谁来触发(谁是事件的发布者),而第二个参数则是我们刚刚定义的事件参数,它只有一个属性,就是事件的触发时间。函数签名(委托)已经定义好了,接下来需要对事件进行定义。
二、定义事件
在C#中,使用event关键字定义事件。事件定义的形式是:“<modifier> event <event_handler> name”,其中modifier是大家熟知的访问修饰符,也就是“public”、“protected”等,event_handler是定义了事件处理函数签名的委托(在本例中,也就是上面的ServerEventHandler),name自然就是这个事件的名称了。由此,我们的两个事件(成功启动事件和成功停止事件)可以定义如下:
- /// <summary>
- /// 定义一个事件,当服务器正常启动后,触发该事件
- /// </summary>
- public event ServerEventHandler Started;
- /// <summary>
- /// 定义一个事件,当服务器正常结束后,触发该事件
- /// </summary>
- public event ServerEventHandler Stopped;
三、触发事件
事件当然由其发布者触发。考察服务器“Server”这个对象,启动和停止是其本身应有的操作,因此,启动与停止是否成功,也就只有它自己知道。那么成功启动与成功停止的事件自然由其自身引发。事件的触发可以在对象中的任何地方发生,比如在本例中,我们可以在Start方法最后部分调用Started事件,而在Stop方法的最后部分调用Stopped事件。从扩展性方面考虑,我们还是把事件的触发单独放到一个protected方法中,这样做的好处是,当我们对“Server”进行扩展的时候,我们还可以重写这个protected方法,以便在事件触发之前再进行其它特殊的操作。因此,我们的事件触发部分就实现如下:
- protected virtual void DoStarted(object sender, ServerEventArgs e)
- {
- if (Started != null)
- Started(sender, e);
- }
- protected virtual void DoStopped(object sender, ServerEventArgs e)
- {
- if (Stopped != null)
- Stopped(sender, e);
- }
- /// <summary>
- /// 执行服务器的启动操作
- /// </summary>
- public void Start()
- {
- // TODO: 在此启动服务器
- DoStarted(this, new ServerEventArgs(DateTime.Now));
- }
- /// <summary>
- /// 执行服务器的停止操作
- /// </summary>
- public void Stop()
- {
- // TODO: 在此停止服务器
- DoStopped(this, new ServerEventArgs(DateTime.Now));
- }
由此,服务器在成功启动后,就会调用DoStarted方法,进而触发Started事件;服务器的停止操作也与此类似。我们需要注意到DoStarted和DoStopped方法中的条件判断语句,该语句是用来检查Started和Stopped事件列表中是否有订阅,如果有则触发事件。这种做法是很有必要的,因为并非所有的访问者都会去订阅事件。
四、订阅事件
在订阅者内订阅事件,只需要将事件处理函数添加到相应的事件中即可。在C#中使用“+=”运算符将事件处理函数添加到事件,而使用“-=”运算符将事件处理函数从事件中删除。下面的代码初始化了一个服务器,订阅了该服务器的成功启动与成功停止事件,并试图启动和停止服务。我们可以看到,在服务器成功启动和成功停止完成后,系统会输出启动或停止的具体时间。
- class Program
- {
- static void Main(string[] args)
- {
- Server server = new Server();
- // 订阅成功启动的事件
- server.Started += new Server.ServerEventHandler(server_Started);
- // 订阅成功停止的事件
- server.Stopped += new Server.ServerEventHandler(server_Stopped);
- // 启动服务
- server.Start();
- // 休息3秒钟,以模拟服务的处理时间
- Thread.Sleep(3000);
- // 停止服务
- server.Stop();
- }
- static void server_Stopped(object sender, ServerEventArgs e)
- {
- Console.WriteLine("Server successfully stopped at: {0}", e.FireDateTime);
- }
- static void server_Started(object sender, ServerEventArgs e)
- {
- Console.WriteLine("Server successfully started at: {0}", e.FireDateTime);
- }
- }
本文简要介绍了事件的概念、事件的定义、触发以及订阅的相关内容。有关事件的其它内容,比如EventHandler和EventHandler<T>委托、事件处理函数列表、接口内事件的实现等,将在后续的文章中一一介绍。
本文相关演示源代码:点击下载此文件