zoukankan      html  css  js  c++  java
  • 从多播委托到事件

    一、多播委托

    前文提到的委托只是在一个委托类型中存储了一个方法(函数),实际上一个委托变量可以同时绑定多个方法,这些委托形成了一个委托链,每一个委托(实际上是方法)都顺序指向下一个委托,这个委托链就是多播委托。

    每一个绑定的方法就像是订阅者一样,等着发布者的消息,而触发委托变量的那个就像是发布者,将出发的信号传给所有的订阅者。

    1、订阅者

    考虑一个温度控制器的例子,这个控制器拥有两个调温器,一个加热器,当温度低于指定的值时,启动,一个冷却器,当温度高于指定的温度时,启动。二者的类设计如下:

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Linq;
     4 using System.Text;
     5 //订阅者
     6 namespace DuoBoEvent
     7 {
     8     class Heater
     9     {
    10         public Heater(float temperature)//设定启动加热器的临界
    11         {
    12             Temperature = temperature; 
    13         }
    14         private float _Temperature;
    15 
    16         public float Temperature
    17         {
    18             get { return _Temperature; }
    19             set { _Temperature = value; }
    20         }
    21         public void OnTemperatureChanged(float newTemperature)
    22         {
    23             if (newTemperature < Temperature)
    24             {
    25                 Console.WriteLine("Heater start");
    26             }
    27             else
    28             {
    29                 Console.WriteLine("Heater stop");
    30             }
    31         }
    32     }
    33 }
    Heater
     1 using System;
     2 using System.Collections.Generic;
     3 using System.Linq;
     4 using System.Text;
     5 //订阅者
     6 namespace DuoBoEvent
     7 {
     8     class Cooler
     9     {
    10         private float _Temperature;//启动Cooler的临界温度
    11 
    12         public float Temperature
    13         {
    14             get { return _Temperature; }
    15             set { _Temperature = value; }
    16         }
    17         public Cooler(float temperature)
    18         {
    19             Temperature = temperature;
    20         }
    21 
    22         public void OnTemperatureChanged(float newTemperature)//传入的为当前温度
    23         {
    24             if (newTemperature > Temperature)
    25             {
    26                 Console.WriteLine("Cooler start");
    27             }
    28             else
    29             {
    30                 Console.WriteLine("Cooler stop");
    31             }
    32         }
    33     }
    34 }
    Cooler

    可以看出,这两个类除了温度比较外,几乎完全一致,温度比较的那个函数,形式也是一致的。OnTemperatureChanged都属于订阅者方法,他们的参数类型与发布者的委托类型一致,这样才可以绑定到发布者的委托变量上。

    2、发布者

    发布者代码如下:

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Linq;
     4 using System.Text;
     5 
     6 namespace DuoBoEvent
     7 {//发布者
     8     class Thermostat
     9     {
    10         public delegate void TemperatureChangedHandler(float newTemperature);//定义一个委托,他与订阅者的函数签名一致
    11 
    12         //定义一个委托类型的成员,用来发布“消息”
    13         private TemperatureChangedHandler _OnTemperatureChange;
    14 
    15         public TemperatureChangedHandler OnTemperatureChange
    16         {
    17             get { return _OnTemperatureChange; }
    18             set { _OnTemperatureChange = value; }
    19         }
    20         //定义这个属性,在其访问器内出发委托,作为发布
    21         private float _CurrentTemperature;
    22 
    23         public float CurrentTemperature
    24         {
    25             get { return _CurrentTemperature; }
    26             set 
    27             {
    28                 if (value != CurrentTemperature)
    29                 {
    30                     //其实,_CurrentTemperature已经是输入新的温度后的前温度了,用来与新的温度Value比较,下一句就是更新前温度为现温度
    31                     _CurrentTemperature = value;//用作一个参数,类似 public void OnTemperatureChanged(float newTemperature)里面的参数
    32                     //调用委托
    33                     OnTemperatureChange(value);//如果温度不一致再调用
    34                 } 
    35                  
    36             }
    37         }
    38 
    39     }
    40 }
    Thermostat

    可以看出,在_CurrentTemperature的访问器内调用了这个委托变量,如果我在主函数中将两个调节器的OnTemperatureChanged与Thermostat的委托变量绑定后,当执行到_CurrentTemperature的访问器后,通过判定,会调用委托。

    3、主函数:

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Linq;
     4 using System.Text;
     5 
     6 namespace DuoBoEvent
     7 {
     8     class Program
     9     {
    10         static void Main(string[] args)
    11         {
    12             Heater heater = new Heater(60);
    13             Cooler cooler = new Cooler(80);
    14             Thermostat thermostat = new Thermostat();
    15             string temperature;
    16             //订阅者与发布者绑定
    17             thermostat.OnTemperatureChange += heater.OnTemperatureChanged;
    18             thermostat.OnTemperatureChange += cooler.OnTemperatureChanged;
    19             //读入当前温度,60及其以下会出发Heater,80及其以上,会出发Cooler
    20             Console.WriteLine("Enter the current temperature");
    21             temperature = Console.ReadLine();
    22             thermostat.CurrentTemperature = int.Parse(temperature);
    23         }
    24     }
    25 }
    main

    在最后一句,在赋值的同时(实际上是晚于复制),调用了委托,便可以看到两个调节器的状态。

    二、对发布者调用委托变量的考虑

    如果我在主函数中没有给Thermostat的委托变量指定任何方法呢?会提示委托对象未实例化,所以在调用前必须要进行空值检查,代码如下:

     1   if (value != CurrentTemperature)
     2                 {
     3                     //其实,_CurrentTemperature已经是输入新的温度后的前温度了,用来与新的温度Value比较,下一句就是更新前温度为现温度
     4                     _CurrentTemperature = value;//用作一个参数,类似 public void OnTemperatureChanged(float newTemperature)里面的参数
     5                     //调用委托
     6                     TemperatureChangedHandler localOnChange = OnTemperatureChange;
     7                     if (localOnChange != null)
     8                     {
     9                         OnTemperatureChange(value);//如果温度不一致再调用
    10                     }   
    11                 } 
    Update

    需要注意的是,并没有直接检查OnTemperatureChange,而是将其赋值给了localOnChange,因为可能在判断的时候,其他线程置空了OnTemperatureChange,造成错误。

    既然委托是引用类型,那么为什么置空OnTemperatureChange不会影响到localOnChange呢?因为C#中,删除订阅者通过-=运算符,而调用-=来处理OnTemperatureChange-=<listener>,不会从OnTemperatureChange上删除,而是生成了一个全新的委托指向他,所以localOnChange是安全的。

    三、BUG的处理。

    如果同时绑定了2个方法,但是第一个方法在执行的出现了BUG导致不能正常运行,系统如何处理呢?

    解决方法是,在Thermostat中调用委托的时候,对其进行遍历,便可以继续通知后续的方法。

    代码如下:

     1    if (localOnChange != null)
     2                     {
     3                         foreach (TemperatureChangedHandler handler in OnTemperatureChange.GetInvocationList())
     4                         {
     5                             try
     6                             {
     7                                 handler(value);
     8                             }
     9                             catch (System.Exception ex)
    10                             {
    11                                 Console.WriteLine(ex.Message);
    12                             }
    13                         }
    14                        // OnTemperatureChange(value);//如果温度不一致再调用
    15 
    16                     }   
    ThermostatUpdate2

    若委托有返回值,也需要用遍历循环的方法来处理返回值。

    四、事件

    从上文可以看到,委托在Main里面可以用+=,也可以用-=,但是有些程序员会不小心将+= -=写成=,而=的机制是,先将左侧的委托变量清空,再将右侧的方法复制给左侧,这样会造成严重的错误。所以有必要对委托进行另一次封装,阻止其在Mian中的=操作。

    另外一个问题是,不仅可以在Thermostat中触发事件,也可以main函数中显式的触发事件,如下语句:Thermostat.OnTemperature(42); 这样完全越过了发布者里面的各种判断,容易造成错误。而事件是不允许这样做的。

    使用了事件,就成了事件-编码模式,发布者代码改成如下:

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Linq;
     4 using System.Text;
     5 
     6 namespace DuoBoEvent
     7 {
     8     class ThermostatEvent
     9     {
    10         //此类是自定义的参数类
    11         public class TemperatureAtgs : System.EventArgs
    12         {
    13             public TemperatureAtgs(float newTemperature)
    14             {
    15                 NewTemperature = newTemperature;
    16             }
    17             private float _newTemperature;
    18 
    19             public float NewTemperature
    20             {
    21                 get { return _newTemperature; }
    22                 set { _newTemperature = value; }
    23             }
    24         }
    25         //声明一个委托,符合标准的参数列表
    26         public delegate void TemperatureChangedHandler(object sender, TemperatureAtgs newTemperature);
    27         //用事件字段封装一个事件属性
    28         public event TemperatureChangedHandler OnTemperatureChange = delegate { };//与委托不同,此处声明为了public,看似减弱封装,实际上event给了更强的限制。
    29         //同多播
    30         private float _CurrentTemperature;
    31 
    32         public float CurrentTemperature
    33         {
    34             get { return _CurrentTemperature; }
    35             set
    36             {
    37                 if (value != CurrentTemperature)
    38                 {
    39                    
    40                     _CurrentTemperature = value;
    41                     //调用事件
    42                     TemperatureChangedHandler localOnChange = OnTemperatureChange;
    43                     if (localOnChange != null)
    44                     {                 
    45                         OnTemperatureChange(this,new TemperatureAtgs(value));//在主函数里绑定了之后,便可以调用Cooler与Heater的指定的函数了
    46                     }
    47                 }
    48 
    49             }
    50         }
    51     }
    52 }
    ThermostatEvent

    定义与调用不同,其中都用注释标明了。当然,调用的时候应该把响应的订阅者的函数形式改成与事件一致,即形式如下:

    1   public void OnTemperatureChanged(object sender, ThermostatEvent.TemperatureAtgs newTemperature)

    主函数可以不变。主要是拒绝了在包容类外的=操作与触发操作。

    五、泛型事件

    如果事件的第二个参数发生了改变,是不是需要另外定义一个事件呢?当然不用,微软提供了泛型委托,只要指明类型就好了。

    原型定义如下:

    public delegate void EventHandler<T>(object sender,T e)where T:EventArgs 
    where T:EventArgs 保证了T只能继承自EventArgs 
    1   //声明一个委托,符合标准的参数列表
    2     //    public delegate void TemperatureChangedHandler(object sender, TemperatureAtgs newTemperature);
    3         //用事件字段封装一个事件属性
    4      //   public event TemperatureChangedHandler OnTemperatureChange = delegate { };//与委托不同,此处声明为了public,看似减弱封装,实际上event给了更强的限制。
    5         //用泛型委托
    6         public event EventHandler<TemperatureAtgs> OnTemperatureChange;
    泛型委托

    定义委托的两句就可以变成了一句,如上。

    至于判断中的那个

     //TemperatureChangedHandler localOnChange = OnTemperatureChange;

    可以注释掉,因为不存在TemperatureChangedHandler 了,其他错误提示很容易改掉,不再赘述。

  • 相关阅读:
    GitLab 介绍
    git 标签
    git 分支
    git 仓库 撤销提交 git reset and 查看本地历史操作 git reflog
    git 仓库 回退功能 git checkout
    python 并发编程 多进程 练习题
    git 命令 查看历史提交 git log
    git 命令 git diff 查看 Git 区域文件的具体改动
    POJ 2608
    POJ 2610
  • 原文地址:https://www.cnblogs.com/tntboom/p/4007114.html
Copyright © 2011-2022 走看看