委托的应用之二:使用多播委托编码Observer模式。以及事件的引入

    下面将通过一个例子来讲解如何使用多播委托编码Observer模式。

    问题描述:一个加热器和一个冷却器连接到同一个自动调温器。为了控制加热器和冷却器的打开和关闭,要向它们通知温度的变化。自动调温器将温度的变化发布给多个订阅者:加热器和冷却器。

   (1)定义订阅者:加热器和冷却器 的方法

 (2)定义发布者类:自动调温器

(3)在主程序中连接发布者和订阅者,改变自动调温器的当前温度。

 

上述使用多播委托来编码Observer模式时存在以下问题:

问题1、在发布者类Thermostat中当当前温度值改变而调用委托方法时,没有检查委托变量是否为空。

      假如当前没有订阅者注册接收通知,则OnTemperatureChange为空,执行OnTemperatureChange(value)语句会引发一个NUllReferenceException。因此需要将类Thermostat中CurrentTemperature属性的 set访问器代码做如下修改:

     set

   {

         if ( value!=CurrentTemperature )

           {

                   _currentTemperature = value;

                   TemperatureChangeHandler  localOnChange= OnTemperatureChange;

                    if(localOnChange!=null)

                            localOnChange(value);

           }

  }

 

  说明:并没有在一开始就检查委托变量OnTemperatureChange是否为空值,而是先将OnTemperatureChange赋值给另一个委托变量localOnChange。这个简单的修改可以确保在检查空值和发送通知之间,假如所有OnTemperatureChange订阅者都被移除(由一个不同的线程),那么不会触发NUllReferenceException异常。

  再次提醒:在调用一个委托之前,要检查它的值是否为空。

问题2:假如一个订阅者引发了一个异常,那么后续的订阅者就接收不到发布者发出的通知。

       由于委托的调用列表中的方法是顺序调用而不是同时调用的,所以当一个订阅者引发了异常,那么后续的订阅者就接收不到发布者发出的通知。为了避免这个问题,使所有的订阅者都能收到通知(不管之前的订阅者有过什么行为),必须手动遍历订阅者列表,并单独调用它们。

     将类Thermostat中CurrentTemperature属性的 set访问器代码做如下修改:

     set

   {

         if ( value!=CurrentTemperature )

           {

                   _currentTemperature = value;

                   TemperatureChangeHandler  localOnChange= OnTemperatureChange;

                    if(localOnChange!=null)

                         {

                              foreach ( TemperatureChangeHandler handler in localOnChange.GetInvocationList()  )

                              {

                                    try

                                       {  handler(value); }

                                   catch (Exception  exception)

                                      { Console.WriteLine(exception.Message);}

                                 }

                           }

                     }

}

         代码说明:从一个委托的GetInvocationList()方法可以获得一份订阅者列表。枚举该列表中的每一项,可以返回单独的订阅者。若随后将每个订阅者调用都放到一个try/catch块中,就可以先处理好任何出错的情形,再继续循环迭代。这样尽管某个订阅者引发了异常,随后的订阅者也能收到温度改变的通知。

 

问题3、当委托方法有返回值或参数为引用类型时,也必须遍历委托调用列表,而并非直接激活一个通知。

            因为调用一个委托,就有可能造成将一个通知发给多个订阅者。假如订阅者方法有返回值,就无法确定应该使用哪一个订阅者的返回值。类似地,当委托方法的参数为引用类型时也需要特殊处理。

 

事件的作用

     到目前为止使用的委托存在两个关键的问题。c#使用关键字event来解决这些问题。

1、封装订阅

      错误地使用“=”而不是“+=”

class Program
    {
        public static void Main()
        {
            Thermostat thermostat = new Thermostat();
            Heater heater = new Heater(60);
            Cooler cooler = new Cooler(80);
            string temperature;

            thermostat.OnTemperatureChange = heater.OntemperatureChanged;

            //bug: assignment operator overrides previous assignment
            thermostat.OnTemperatureChang = cooler.OnTemperatureChanged;
        }


    上述代码与原来代码的唯一区别,就是它不是使用“+=”运算符,而是使用一个简单的赋值运算符。其结果就是将cooler.OnTemperatureChanged赋值给OnTemperatureChange时,heater.OnTemperatureChanged会被移除,因为一个全新的委托链替代了之前的链。在本该使用“+=”的地方使用了“=”,由于这是一个十分容易犯的错误,所以最好的办法就是根本不为包容类(声明委托的类)以外的对象提供对赋值运算符的支持。event关键字的目的就是提供额外的封装,避免你不小心的取消其他订阅者。

 

2、封装发布

   委托和事件的第二个重要区别在于,事件确保只有包容类才能触发一个事件通知。

  class Program
    {
        public static void Main()
        {
            Thermostat thermostat = new Thermostat();
            Heater heater = new Heater(60);
            Cooler cooler = new Cooler(80);
            string temperature;

            thermostat.OnTemperatureChange = heater.OntemperatureChanged;

            //bug: assignment operator overrides previous assignment
            thermostat.OnTemperatureChang+ = cooler.OnTemperatureChanged;

            thermostat .OnTemperatureChange(42);
        }
    }


上述代码 从事件包容者外部触发了事件。即使thermostat的CurrentTemperature没有发生变化,Program也能调用OnTemperatureChange 委托。因此,Program触发了对所有thermostat订阅者的一个通知,告诉它们温度已发生改变,而实际上thermostat的温度没有变化。和之前问题一样,委托的问题在于封装的不充分。Thermostat应禁止其他任何类调用OnTemperatureChang委托。

你可能感兴趣的:(c#,exception,string,class,null,c#)