《Effective C#》读书笔记——条目25:用事件模式实现通知<使用C#表达设计>
.NET中的事件其实就是一个观察者模式(Observer Pattern)的一个语法上的快捷实现(更多可以参考:使用委托和事件实现观察者模式)。事件是一种内建的委托,用来为事件处理函数提供类型安全的方法签名。事件就是对象将信息告知观察者的方式。
1.发布者定义事件
我们来看一个例子,有一个日志类,将应用程序需要分发的信息发送个各个侦听者,这些侦听者可以是控制域、系统日志、数据库等等,首先定义一个在事件触发中负责传递消息的事件参数类:
1 public class LoggerEventArgs : EventArgs 2 { 3 public string message { get; private set; } 4 public int Priority { get; private set; } 5 6 public LoggerEventArgs(int p, string m) 7 { 8 this.Priority = p; 9 this.message = m; 10 } 11 }
然后是日志类本身:
1 public class Logger 2 { 3 static Logger() 4 { 5 theOnly = new Logger(); 6 } 7 8 private Logger() 9 { 10 } 11 12 private static Logger theOnly = null; 13 public static Logger Singleton 14 { 15 get { return theOnly; } 16 } 17 //定义事件 18 public event EventHandler<LoggerEventArgs> Log; 19 //在这里增加消息和日志 20 public void AddMsg(int priority, string msg) 21 { 22 //这里引用临时变量是一个重要的安全措施,可预防多线程环境中的竞争条件 23 //若是没有引用的副本,客户代码可能会在if判断语句和事件处理函数之间移 24 //除事件处理函数,而复制引用之后即可避免这种情况 25 EventHandler<LoggerEventArgs> l = Log; 26 if (l != null) 27 l(this, new LoggerEventArgs(priority, msg)); 28 } 29 }
在这里AddMsg()是触发事件的方法,LoggerEventArgs类中定义了事件的优先级和消息内容,委托则为事件处理函数定义了签名。在Logger类内部,事件自动Log定义了事件处理函数。编译器看到这个字段后会自动创建对应的Add和Remove操作符,编译器生成的代码和下面类似:
1 public class Logger 2 { 3 private EventHandler<LoggerEventArgs> log; 4 5 public event EventHandler<LoggerEventArgs> Log 6 { 7 add { log = log + value; } 8 remove { log = log - value; } 9 } 10 public void AddMsg(int priority, string msg) 11 { 12 EventHandler<LoggerEventArgs> l = log; 13 if (l != null) 14 l(this, new LoggerEventArgs(priority, msg)); 15 } 16 }
或者我们可以直接查看IL代码:
2.侦听者订阅事件
我们可以把日志信息订阅到标准错误控制台输出:
1 class ConsoleLogger 2 { 3 static ConsoleLogger() 4 { 5 Logger.Singleton.Log += (sender, msg) => 6 { 7 Console.Error.WriteLine("{0}:\t{1}", msg.Priority.ToString(), msg.message); 8 }; 9 } 10 }
或者是直接将日志记录到系统日志中:
1 class EventLogger 2 { 3 private static Logger logger = Logger.Singleton; 4 private static string eventSource; 5 private static EventLog logDest = new EventLog(); 6 7 static EventLogger() 8 { 9 logger.Log += (sender, msg) => 10 { 11 if (logDest != null) 12 logDest.WriteEntry(msg.message, EventLogEntryType.Information, msg.Priority); 13 }; 14 } 15 16 public static string Evensource 17 { 18 get { return eventSource; } 19 set 20 { 21 eventSource = value; 22 if (!EventLog.SourceExists(eventSource)) 23 EventLog.CreateEventSource(eventSource, "Application"); 24 25 if (logDest != null) 26 { 27 logDest.Dispose(); 28 logDest = new EventLog(); 29 logDest.Source = eventSource; 30 } 31 } 32 } 33 }
运行程序:
1 ConsoleLogger c = new ConsoleLogger(); 2 EventLogger.Evensource = "事件源?"; 3 EventLogger ee = new EventLogger(); 4 Logger.Singleton.AddMsg(10086, "这个Logger类创建的日志");
3.动态创建事件对象
前面的Logger类只包含了一个事件,但有时候也有一些类(Windows控件)包含的事件数量非常多,这种情况下,为每个事件都定义一个字段的做法会显得比较臃肿。在某些情况下,只要很少的事件会在程序中真正起到作用,这时候我们需要根据运行时的需要来动态创建事件对象。
根据前面的Logger类我们对其进行扩展,想Logger类中添加子系统,可以为每个子系统创建一个事件。客户则会注册到子系统中的事件。扩展后的Logger类包含了System.ComponentModel.EventHandlerList容器,存放所有的事件对象。
1 public sealed class Logger 2 { 3 private static EventHandlerList Handlers = new EventHandlerList(); 4 5 static public void AddLogger(string system, EventHandler<LoggerEventArgs> ev) 6 { 7 Handlers.AddHandler(system, ev); 8 } 9 10 static public void RemoveLogger(string system, EventHandler<LoggerEventArgs> ev) 11 { 12 Handlers.RemoveHandler(system, ev); 13 } 14 15 /// <summary> 16 /// 接收一个system字符串参数,用于指定产生日志的子系统 17 /// 如果子系统有侦听者,事件会触发 18 /// 同样,如果一个事件侦听者注册了所有消息,那么它的事件也会被触发 19 /// </summary> 20 /// <param name="system"></param> 21 /// <param name="priority"></param> 22 /// <param name="msg"></param> 23 static public void AddMsg(string system, int priority, string msg) 24 { 25 if (!string.IsNullOrEmpty(system)) 26 { 27 EventHandler<LoggerEventArgs> l = Handlers[system] as EventHandler<LoggerEventArgs>; 28 29 LoggerEventArgs args = new LoggerEventArgs(priority, msg); 30 if (l != null) 31 l(null, args); 32 l = Handlers[""] as EventHandler<LoggerEventArgs>; 33 if (l != null) 34 l(null, args); 35 } 36 } 37 }
前面的EventHandlerList没有提供泛型的版本,所以其中有很多的类型转换操作。当客户代码关联到一个特定的子系统上,新的事件就会被创建。对于同一个子系统的后续请求会获取相同的事件对象。如果我们的类中有大量的事件,应该考虑使用这种事件处理函数集合。仅当客户代码真正注册有事件处理函数时,才会创建事件成员。在.NET Framework内部,System.Windows.Forms.Control类使用了一种复杂的方式,进而隐藏所有事件字段操作的复杂性。
EventHandlerList没有提供内建的泛型实现,我们可以基于Dictionary自行构造,泛型版本降低了类型转换的工作,但也增加了一些用来映射事件的代码,具体使用哪种方式,可以根据实际情况来考量:
1 public sealed class Logger 2 { 3 private static Dictionary<string, EventHandler<LoggerEventArgs>> Handlers = new Dictionary<string, EventHandler<LoggerEventArgs>>(); 4 5 static public void AddLogger(string system, EventHandler<LoggerEventArgs> ev) 6 { 7 if (Handlers.ContainsKey(system)) 8 Handlers[system] += ev; 9 else 10 Handlers.Add(system, ev); 11 } 12 13 static public void RemoveLogger(string system, EventHandler<LoggerEventArgs> ev) 14 { 15 Handlers[system] -= ev; 16 } 17 18 static public void AddMsg(string system, int priority, string msg) 19 { 20 if (!string.IsNullOrEmpty(system)) 21 { 22 EventHandler<LoggerEventArgs> l = null; 23 Handlers.TryGetValue(system, out l); 24 25 LoggerEventArgs args = new LoggerEventArgs(priority, msg); 26 if (l != null) 27 l(null, args); 28 //空字符串意味着接收所有消息 29 l = Handlers[""] as EventHandler<LoggerEventArgs>; 30 if (l != null) 31 l(null, args); 32 } 33 } 34 }
小节
事件提供了一种标准的机制来通知侦听者。.NET的事件模式使用了事件语法来实现观察者模式。任意数量的客户对象都可以将自己的处理函数注册到事件上,然后处理这些事件。这些客户对象不需要在编译器就给出,事件也不必非有订阅者才能正常工作。在C#中使用事件可以降低发送者和可能的通知接收者之间的耦合。发送者完全可以独立于接收者进行开发。事件是实现广播类型行为的标准方式。