zoukankan      html  css  js  c++  java
  • 《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     }

    或者是直接将日志记录到系统日志中:

    View Code
     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容器,存放所有的事件对象。

    View Code
     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自行构造,泛型版本降低了类型转换的工作,但也增加了一些用来映射事件的代码,具体使用哪种方式,可以根据实际情况来考量:

    View Code
     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#中使用事件可以降低发送者和可能的通知接收者之间的耦合。发送者完全可以独立于接收者进行开发。事件是实现广播类型行为的标准方式。

  • 相关阅读:
    1.IntelliJ IDEA搭建SpringBoot的小Demo
    etc目录名字的意思---挖Linux中的古老缩略语
    CI当开启URL重写的时候,报错500 Internal Server Error
    app后端架构设计(转)
    Redis安装及主从配置
    ***Linux文件夹文件创建、删除、改名
    Redis中常用命令
    linux上ln链接命令详细说明
    Redis快速入门:安装、配置和操作
    redis的PHP扩展包安装方法
  • 原文地址:https://www.cnblogs.com/IPrograming/p/EffectiveCSharp_25.html
Copyright © 2011-2022 走看看