一、多播委托
前文提到的委托只是在一个委托类型中存储了一个方法(函数),实际上一个委托变量可以同时绑定多个方法,这些委托形成了一个委托链,每一个委托(实际上是方法)都顺序指向下一个委托,这个委托链就是多播委托。
每一个绑定的方法就像是订阅者一样,等着发布者的消息,而触发委托变量的那个就像是发布者,将出发的信号传给所有的订阅者。
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 }
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 }
可以看出,这两个类除了温度比较外,几乎完全一致,温度比较的那个函数,形式也是一致的。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 }
可以看出,在_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 }
在最后一句,在赋值的同时(实际上是晚于复制),调用了委托,便可以看到两个调节器的状态。
二、对发布者调用委托变量的考虑
如果我在主函数中没有给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 }
需要注意的是,并没有直接检查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 }
若委托有返回值,也需要用遍历循环的方法来处理返回值。
四、事件
从上文可以看到,委托在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 }
定义与调用不同,其中都用注释标明了。当然,调用的时候应该把响应的订阅者的函数形式改成与事件一致,即形式如下:
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 了,其他错误提示很容易改掉,不再赘述。