在现实生活当中,有一些事情发生时,会连带另一些事情的发生。例如,当某国的总统发生换届时,不同党派会表现出不同的行为。两者构成了“因果”关系,因为发生了A,所以发生了B。在编程语言当中,具有类似的概念。在编程语言中,发生的事情 A 称为事件,因 A 发生的事情 B 称为对事件的处理或者响应(事件处理程序)。
本章主要讲解 C# 语言当中的发布者-订阅者模式的概念、代码实现该模式时的组成部分以及标准事件的用法。
1. 发布者和订阅者的概念
在程序中,我们很多时候会面临这样一个需求:当一个特定的程序事件发生时,程序的其它部分(类、函数或者其它)可以得到该事件已经发生的通知并对此做出相应的处理。
发布者-订阅者模式(publisher/subscriber pattern)可以满足这种需求。在这种模式中,发布者类定义了订阅者类感兴趣的事件。当事件发生时,发布者通知到这些订阅者,然后订阅者执行相应的事件处理函数。
但是,发布者是如何通知到这些订阅者呢?订阅者通过注册函数。其实就是发布者维护了一个关注某个事件的订阅者集合。例如,我在 csdn 博客上开了某个领域的专栏,然后你订阅了这个专栏,在后台会维护关注这个专栏的用户列表。当我更新了这个专栏的文章时,会通知到每一个订阅的用户。
那么,订阅者又是如何在事件发生时执行相应的事件处理函数呢?订阅者向发布者注册事件发生时的事件处理函数(回调函数,我们称函数的参数类型是函数的函数)。
下图说明了发布者-订阅者模式的工作流程:
下面是发布者-订阅者模式的组件:
- 发布者:发布某个事件的类或结构。维护一个订阅者集合(可选)、一个事件处理程序的集合、提供给订阅者注册订阅和回调函数的接口;
- 订阅者:关注事件的类或者结构。需要向订阅者提供回调函数名;
- 触发事件。本质上是触发事件的代码。
在 C#高级教程(一):委托当中介绍了委托。实际上,事件就像是专门用于某种特殊用途的委托。
2. 源代码组件
为了实现发布者-订阅者模式,在代码中需要完成以下 5 部分:
- 委托类型声明:事件和事件处理程序必须具有相同的签名和返回类型,它们通过委托类型进行描述;
- 事件处理程序声明:订阅者类中会在事件触发时执行的方法调用;
- 事件声明:发布者类必须声明一个订阅者类可以注册的事件成员;
- 事件注册:订阅者必须订阅事件才能在它被触发时得到通知;
- 触发事件的代码:发布者类中“触发”事件并执行所有事件处理程序的代码。
下面是一个简单的代码实现:
delegate void Handler();class Incrementer
{public event Handler CountedADozen; // 事件名public void DoCount(){for (int i = 1; i < 100; i++){if (i % 12 == 0 && CountedADozen!= null) // 触发事件的代码{CountedADozen(); // 调用事件}}}
}// 订阅者
class Dozens
{public int DozensCount { get; set; }public Dozens(Incrementer incrementer){DozensCount = 0;incrementer.CountedADozen += IncrementDozenCount; // 注册回调函数}// 回调函数void IncrementDozenCount(){DozensCount++;}
}class Programer
{static void Main(){Incrementer incrementer = new Incrementer();Dozens dozens = new Dozens(incrementer);incrementer.DoCount();Console.WriteLine("Dozens count: {0}", dozens.DozensCount);}
}
输出:
3. 标准事件的用法
由于 GUI 编程是事件驱动的,而 Windows GUI 编程广泛地使用了事件,因此 .NET 框架提供了一个标准模式。具体就是在 System
命名空间提供了 EventHandler
委托类型。
第二个参数 EventArgs
设计为不能用来传递任何数据,它用于不需要传递数据的事件处理程序。如果你希望传递数据,必须声明一个派生自 EventArgs
的类,使用合适的字段来保存需要传递的数据。
接下来,我们就使用标准事件改写第二节的程序:
class Incrementer
{public event EventHandler CountedADozen; // 事件名public void DoCount(){for (int i = 1; i < 100; i++){if (i % 12 == 0 && CountedADozen!= null) // 触发事件的代码{CountedADozen(this, null); // 调用事件}}}
}
// 订阅者
class Dozens
{public int DozensCount { get; set; }public Dozens(Incrementer incrementer){DozensCount = 0;incrementer.CountedADozen += IncrementDozenCount; // 注册回调函数}// 回调函数void IncrementDozenCount(object sender, EventArgs e){DozensCount++;}
}class Programer
{static void Main(){Incrementer incrementer = new Incrementer();Dozens dozens = new Dozens(incrementer);incrementer.DoCount();Console.WriteLine("Dozens count: {0}", dozens.DozensCount);}
}
通过扩展 EventArgs
来传递数据
为了向自己的事件处理程序的第二个参数传入数据,并且又符合标准惯例,我们需要声明一个派生自 EventArgs
的自定义类,用来保存我们需要传入的数据。
下面是改写后的代码:
// 派生自EventArgs的自定义类
public class IncrementerArgs: EventArgs
{public int IterationCount { get; set; }
}class Incrementer
{// 使用自定义类的泛型委托public event EventHandler<IncrementerArgs> CountedADozen; // 事件名public void DoCount(){IncrementerArgs args = new IncrementerArgs();for (int i = 1; i < 100; i++){if (i % 12 == 0 && CountedADozen!= null) // 触发事件的代码{args.IterationCount = i;CountedADozen(this, args); // 调用事件}}}
}
// 订阅者
class Dozens
{public int DozensCount { get; set; }public Dozens(Incrementer incrementer){DozensCount = 0;incrementer.CountedADozen += IncrementDozenCount; // 注册回调函数}// 回调函数void IncrementDozenCount(object sender, IncrementerArgs e){Console.WriteLine("Incremented at iteration: {0} and {1}",e.IterationCount, sender.ToString());DozensCount++;}
}class Programer
{static void Main(){Incrementer incrementer = new Incrementer();Dozens dozens = new Dozens(incrementer);incrementer.DoCount();Console.WriteLine("Dozens count: {0}", dozens.DozensCount);}
}
输出:
移除事件处理程序
在用完了事件处理程序之后,可以从事件中把它移除。下面是一个例子:
class Publisher
{public event EventHandler SimpleEvent;public void RaiseTheEvent() { SimpleEvent(this, null); }
}class Subsriber
{public void MethodA(object o, EventArgs e) { Console.WriteLine("MethodA called"); }public void MethodB(object o, EventArgs e) { Console.WriteLine("MethodB called"); }}class Program
{static void Main(){Publisher p = new Publisher();Subsriber s = new Subsriber();p.SimpleEvent += s.MethodA;p.SimpleEvent += s.MethodB;p.RaiseTheEvent();Console.WriteLine("\r\nRemove MethodB");p.SimpleEvent -= s.MethodB;p.RaiseTheEvent();}
}
程序的输出:
小结:本章介绍了 C# 语言当中的发布者-订阅者模式的概念,源代码组件的五个部分,以及标准事件的用法。
各位道友,码字不易。如有收获,记得一键三连。