# 原则25:实现通知的事件模式
**By D.S.Qiu**
**尊重他人的劳动,支持原创,转载请注明出处:[http://dsqiu.iteye.com](http://dsqiu.iteye.com)**
.NET 的事件模式无非就是观察者模式的语法规范。(查看 Design Patterns, Gamma, Helm, Johnson, and Vlissides pp.293-303)事件定义类的通知消息。事件是构建在委托之上提供类型安全函数签名的处理。事实上,大多数使用委托的例子就是事件,开发者会认为事件和委托是同一件事。在原则中,我给你介绍了使用委托而不是事件的用法。当你需要通知多个客户告诉他们你的行为,你就可以触发事件。事件就是对象通知观察者。
考虑下面这个简单例子。你正要应用中构建分发消息的日志类。它可以接受所有源头的所有消息并且能够分发给监听者。这些监听者可能是控制台,或者是数据库,或者是系统日志,或者是其他机制。你如下定义这个类,当消息到来就会触发一个事件:
```
public class LoggerEventArgs : EventArgs
{
public string Message { get; private set; }
public int Priority { get; private set; }
public LoggerEventArgs(int p, string m)
{
Priority = p;
Message = m;
}
}
public class Logger
{
static Logger()
{
theOnly = new Logger();
}
private Logger()
{
}
private static Logger theOnly = null;
public static Logger Singleton
{
get { return theOnly; }
}
// Define the event:
public event EventHandler<LoggerEventArgs> Log;
// add a message, and log it.
public void AddMsg(int priority, string msg)
{
// This idiom discussed below.
EventHandler<LoggerEventArgs> l = Log;
if (l != null)
l(this, new LoggerEventArgs(priority, msg));
}
}
```
AddMsg 方法就是恰当触发事件的方式。用临时变量引用日志事件处理器能在多线程程序中保证共享条件安全。如果没有复制引用,在 if 条件检查和事件处理执行之间可以移除事件处理器。通过复制引用,就不会发生。
LoggerEventArgs 包含事件的优先级和消息。委托定义事件处理器。在 Logger 类中,使用 event 域定义事件处理器。编译器检查到 public event 域的定义会自动为你创建 add 和 remove 操作。和你下面的写法产生的代码是一样的:
```
public class Logger
{
private EventHandler<LoggerEventArgs> log;
public event EventHandler<LoggerEventArgs> Log
{
add { log = log + value; }
remove { log = log - value; }
}
public void AddMsg(int priority, string msg)
{
EventHandler<LoggerEventArgs> l = log;
if (l != null)
l(null, new LoggerEventArgs(priority, msg));
}
}
```
C# 编译器为 event 创建 add 和 remove 访问器。我发现 public event 的声明语句会比没有 add/remove 语法更简洁,更容易阅读。所以当你声明 public event 时,让编译器为你创建 add 和 remove 属性。只有当你需要 add 和 remove 做更多事情才需要自己写。
事件不需要掌握任何潜在的监听者的信息。下面的类会自动路由所有消息到标准错误控制台:
```
class ConsoleLogger
{
static ConsoleLogger()
{
Logger.Singleton.Log += (sender, msg) =>
{
Console.Error.WriteLine("{0}:\t{1}", msg.Priority.ToString(), msg.Message);
};
}
}
```
另一个类的实现是直接输出到系统事件日志中:
```
class EventLogger
{
private static Logger logger = Logger.Singleton;
private static string eventSource;
private static EventLog logDest;
static EventLogger()
{
logger.Log += (sender, msg) =>
{
if (logDest != null)
logDest.WriteEntry(msg.Message,EventLogEntryType.Information, msg.Priority);
};
}
public static string EventSource
{
get { return eventSource; }
set
{
eventSource = value;
if (!EventLog.SourceExists(eventSource))
EventLog.CreateEventSource(eventSource, "ApplicationEventLogger");
if (logDest != null)
logDest.Dispose();
logDest = new EventLog();
logDest.Source = eventSource;
}
}
}
```
当有消息产生,事件会通知任何监听的客户端。Logger 不需要关心哪些对象有监听日志事件。
Logger 类只包含了一个事件。有些类(大多数窗口控制器)有大量的事件。在那些例子,为每个事件使用一个域是不可接受的。很多情况,在一个程序中只需要定义少量的事件。如果你遇到这种情况,你可以修改设计只需要在运行时创建的事件对象。
在核心框架的窗口控制子系统包含这样处理的例子。怎么实现呢,添加一个子系统到 Logger 类。每个子系统就是一个事件。客户端只会注册事件到它相关的子系统中。
扩展的 Logger 类有一个 System.ComponentModel.EventHandlerList ,它存储系统支持的所有事件对象。新的 AddMsg 添加 string 参数指定哪个子系统处理日志消息。如果子系统有监听者,事件就会被触发。同时,如果事件监听者对所有消息感兴趣,它的事件也会被触发:
```
public sealed class Logger
{
private static System.ComponentModel.EventHandlerList Handlers = new EventHandlerList();
static public void AddLogger( string system, EventHandler<LoggerEventArgs> ev)
{
Handlers.AddHandler(system, ev);
}
static public void RemoveLogger(string system,EventHandler<LoggerEventArgs> ev)
{
Handlers.RemoveHandler(system, ev);
}
static public void AddMsg(string system, int priority, string msg)
{
if (!string.IsNullOrEmpty(system))
{
EventHandler<LoggerEventArgs> l = Handlers[system] as EventHandler<LoggerEventArgs>;
LoggerEventArgs args = new LoggerEventArgs( priority, msg);
if (l != null)
l(null, args);
// The empty string means receive all messages:
l = Handlers[""] as EventHandler<LoggerEventArgs>;
if (l != null)
l(null, args);
}
}
}
```
新的例子在 EventHandlerList 集合中存储独立的事件处理器。不好的是, EventHandlerList 还没有泛型版本。所以,你在这个例子会看到很多比这本书其他地方要多的类型转换。客户端代码注册具体的子系统,一个新的事件对象就产生了。相同子系统都是检索同一个事件对象。如果你类包含大量的事件接口,你就可以考虑使用事件处理器的集合。把事件对象成员的交给客户端是否注册事件处理器来决定。在 .NET 框架中,System.Windows.Forms.Control 类使用一个会隐藏所有的 event 域的更复杂变种的实现。每个事件都内部访问集合添加和移除具体的处理器。你可以查看 C# 语言规范中了解更多这个语法习惯的细节。
EventHandler 类没有更新到一个泛型版本。你不难用 Dictionary 构建一个自己的实现:
```
public sealed class Logger
{
private static Dictionary<string,
EventHandler<LoggerEventArgs>>
Handlers = new Dictionary<string,
EventHandler<LoggerEventArgs>>();
static public void AddLogger( string system, EventHandler<LoggerEventArgs> ev)
{
if (Handlers.ContainsKey(system))
Handlers[system] += ev;
else
Handlers.Add(system, ev);
}
static public void RemoveLogger(string system,EventHandler<LoggerEventArgs> ev)
{
// will throw exception if system
// does not contain a handler.
Handlers[system] -= ev;
}
static public void AddMsg(string system, int priority, string msg)
{
if (string.IsNullOrEmpty(system))
{
EventHandler<LoggerEventArgs> l = null;
Handlers.TryGetValue(system, out l);
LoggerEventArgs args = new LoggerEventArgs( priority, msg);
if (l != null)
l(null, args);
// The empty string means receive all messages:
l = Handlers[""] as EventHandler<LoggerEventArgs>;
if (l != null)
l(null, args);
}
}
}
```
泛型版本是增加代码提供事件字典和类型转换的权衡。我更喜欢泛型版本,但是它却没能够权衡。
事件提供通知监听者的标准语法。.NET 事件模式就是遵循 event 语法实现观察者模式。任意数量的客户注册处理器到事件并处理它们。这些客户不需要在编译时被知道。事件不需要关心它的订阅者就能正常工作。使用事件可以解耦通知的发送者和可能的接收者。发送者可以独立于接收者开发。事件是广播你的类发生的行为的消息的标准方式。
小结:
这个原则其实就是让大家多用 event ,可以结构消息的发送者和接收者之间关系,有点老死不相往来却彼此为了对方而活。
对于委托 delegate ,C# 衍生出了很多版本:deleage , Action , Func<> , Predicate<> 和 event 。
delegate: 就是原始的委托,可以理解为方法指针或方法的签名
Action: 是没有返回值的泛型委托
Func:有返回值的泛型委托
Predicate<>:返回值为 bool 类型的谓词泛型委托
event:对 delegate 的封装
至于,平常说的匿名函数和 Lambda 表达式跟具体函数一样都是委托的实现方式。
特别地,这个还解决我之前看别人代码的一个困苦:之前又看别人网络层代码老是会用一个临时变量缓存(上文说的用临时变量复制引用),一直不得其解。
欢迎各种不爽,各种喷,写这个纯属个人爱好,秉持”分享“之德!
有关本书的其他章节翻译请[点击查看](/category/297763),转载请注明出处,尊重原创!
如果您对D.S.Qiu有任何建议或意见可以在文章后面评论,或者发邮件(gd.s.qiu@gmail.com)交流,您的鼓励和支持是我前进的动力,希望能有更多更好的分享。
转载请在**文首**注明出处:[http://dsqiu.iteye.com/blog/2085830](/blog/2085830)
更多精彩请关注D.S.Qiu的博客和微博(ID:静水逐风)
- 第一章 C# 语言习惯
- 原则1:使用 属性(Poperty)代替可直接访问的数据成员(Data Member)
- 原则2:偏爱 readonly 而不是 const
- 原则3:选择 is 或 as 而不是强制类型转换
- 原则4:使用条件特性(conditional attribute)代替 #if
- 原则5:总是提供 ToString()
- 原则6:理解几个不同相等概念的关系
- 原则7:明白 GetHashCode() 的陷阱
- 原则8:优先考虑查询语法(query syntax)而不是循环结构
- 原则9:在你的 API 中避免转换操作
- 原则10:使用默认参数减少函数的重载
- 原则11:理解小函数的魅力
- 第二章 .NET 资源管理
- 原则12:选择变量初始化语法(initializer)而不是赋值语句
- 原则13:使用恰当的方式对静态成员进行初始化
- 原则14:减少重复的初始化逻辑
- 原则15:使用 using 和 try/finally 清理资源
- 原则16:避免创建不需要的对象
- 原则17:实现标准的 Dispose 模式
- 原则17:实现标准的 Dispose 模式
- 原则18:值类型和引用类型的区别
- 原则19:确保0是值类型的一个有效状态
- 原则20:更倾向于使用不可变原子值类型
- 第三章 用 C# 表达设计
- 原则21:限制你的类型的可见性
- 原则22:选择定义并实现接口,而不是基类
- 原则23:理解接口方法和虚函数的区别
- 原则24:使用委托来表达回调
- 原则25:实现通知的事件模式
- 原则26:避免返回类的内部对象的引用
- 原则27:总是使你的类型可序列化
- 原则28:创建大粒度的网络服务 APIs
- 原则29:让接口支持协变和逆变
- 第四章 和框架一起工作
- 原则30:选择重载而不是事件处理器
- 原则31:用 IComparable&lt;T&gt; 和 IComparer&lt;T&gt; 实现排序关系
- 原则32:避免 ICloneable
- 原则33:只有基类更新处理才使用 new 修饰符
- 原则34:避免定义在基类的方法的重写
- 原则35:理解 PLINQ 并行算法的实现
- 原则36:理解 I/O 受限制(Bound)操作 PLINQ 的使用
- 原则37:构造并行算法的异常考量
- 第五章 杂项讨论
- 原则38:理解动态(Dynamic)的利与弊
- 原则39:使用动态对泛型类型参数的运行时类型的利用
- 原则40:使用动态接收匿名类型参数