Delegate与Event
Delegate是C#中很有魅力的一种编程方式,在代码中几乎是无处不在。很多园里的朋友已经写过不计其数的delegate相关的随笔,包括用法,概念等等。但是C#本质论这本书里除了基本的用法之外,还指出了很多使用过程当中要注意的事项,以及容易写出Bug的地方。我个人认为这种经验式写作方式很值得推荐,它会让有过类似经验的程序员有比较强烈的共鸣和对书的认同感,从而真正(通常情况下,很多好的建议和规范大家都只是心中认同而后又淡忘掉了)按照书中的‘最佳实践’的建议规范自己的代码。我倾向于在写笔记时记录下自己的联想和思考,对比的学习会让人印象更加深刻。那么下面就进入正题:
C#中的Delegate天然的帮我们实现了Observer模式,它允许将符合格式的方法注册到委托中。当委托被调用时,委托链上已经注册了的方法会依次执行。其实说到这个链式的传播,和Responsibility Chain模式有相似之处,那在下面顺便也简单的比较一下两种模式:
相同点:
在命令触发时,他们都是依次顺序调用链上的对象(方法)。
不同点:
1.Responsibility Chain模式中,每个对象的前后关系都是明确的,每个对象都保留这后一个对象的引用。Observer模式中的对象之间注册时没有顺序关系。
2.Responsibility Chain模式可以提前退出链表,即当消息已经明确得到处理之后,可以返回而不必执行完。Observer模式的目的就是将信息传递到每一个注册了的对象那里,除非这个过程中出现异常,那么直接会退出,而它剩余的对象将得不到任何的相关信息。
3.Responsibility Chain解决的问题是为消息找到合适的处理者而设计的。Observer模式的目的是以广播的形式将消息进行传递。两者解决的问题不同。
好了,扯的有点远,毕竟这篇的主角是Delegate。简单的Delegate实现代码如下:
class Program { static void Main(string[] args) { Thermostat thermostat = new Thermostat(); Condition heater = new Heater(16); Condition cooler = new Cooler(26); //注册 thermostat.OnTemperatureChange += heater.OnConditionTemperatureChange; thermostat.OnTemperatureChange += cooler.OnConditionTemperatureChange; thermostat.CurrentTemperature = 20; Console.Read(); } } public class Thermostat { private float _currentTemperature; public float CurrentTemperature { get { return _currentTemperature; } set { if (value != _currentTemperature) { _currentTemperature = value; OnTemperatureChange(value); } } } //定义委托 public delegate void TemperatureChangeHanlder(float newTemp); private TemperatureChangeHanlder _onTemperatureChange; //暴露给订阅者 public TemperatureChangeHanlder OnTemperatureChange { get { return _onTemperatureChange; } set { _onTemperatureChange = value; } } } public abstract class Condition { public float ConditionTemperature { get; set; } public Condition(float temp) { ConditionTemperature = temp; } public abstract void OnConditionTemperatureChange(float newTemp); } class Heater:Condition { public Heater(float value):base(value) { } //实现订阅的方法 public override void OnConditionTemperatureChange(float newTemp) { if (newTemp > ConditionTemperature) { Console.WriteLine("Temperature is {0},heater will turn off.", newTemp); } else { Console.WriteLine("Temperature is {0},heater will turn on.", newTemp); } } } class Cooler:Condition { public Cooler(float value):base(value) { } public override void OnConditionTemperatureChange(float newTemp) { if (newTemp > ConditionTemperature) { Console.WriteLine("Temperature is {0},cooler will turn on.", newTemp); } else { Console.WriteLine("Temperature is {0},cooler will turn off.", newTemp); } } }
开篇的时候说过,这篇文章不是说概念的,那么要使用Delegate需要注意的问题都有哪些呢:
1.检查空值
假如在委托执行时,没有订阅者注册过这个委托,即OnTeperatureChange为空,则会引发NullReferenceException。当然检查空值的方式也很有讲究,如果在检查时不为空,但是执行过程中订阅者全部取消了订阅那同样会抛出异常,那么是否在执行时需要通过加锁的方式保证委托链上的订阅者不会发生变化呢?答案是否定的,具体做法如下:
public class Thermostat { private float _currentTemperature; public float CurrentTemperature { get { return _currentTemperature; } set { if (value != _currentTemperature) { _currentTemperature = value; TemperatureChangeHanlder newHandler = OnTemperatureChange; if (newHandler != null) newHandler(value); } } } //定义委托 public delegate void TemperatureChangeHanlder(float newTemp); private TemperatureChangeHanlder _onTemperatureChange; //暴露给订阅者 public TemperatureChangeHanlder OnTemperatureChange { get { return _onTemperatureChange; } set { _onTemperatureChange = value; } } }
可能看到这里,你会有一点疑问,难道传递个引用就能保证订阅者不变吗?有疑问的地方就是有知识点的地方,就是我们下一个需要讲的。
2.注意+=,-=等运算符
Delegate是不变的,那么=,+=等等这些操作究竟是怎么回事呢?其实他们不会对原有的委托产生任何影响,而是会重新返回一个全新的委托。这就是委托的不变性,类似的不变性可以参考String类型。当然,在书写时也必须要十分小心,如果将+=或者-=写成了=,那在‘无形中’引入一个bug。
3.异常处理
这里需要格外注意,因为委托链是依次执行,在执行过程中出现异常将导致后面的订阅者无法接收到消息并执行。为了避免异常的中断,有两种方式进行处理:
第一就是依次调用订阅者的方法,并在调用时处理异常。代码如下:
private float _currentTemperature; public float CurrentTemperature { get { return _currentTemperature; } set { if (value != _currentTemperature) { _currentTemperature = value; foreach (TemperatureChangeHanlder item in OnTemperatureChange.GetInvocationList()) { try { item(value); } catch (Exception) { // do something } } } } }
第二当然就是让每个订阅的方法管好自己,自己把自己的异常给处理掉。代码就不写出来了。
4.返回值和ref、out
其实以前在看C#本质论之前,我都没有在意过这个问题,但是看看确实还是有意义的。不知道大家发现了没有,委托一般都是没有返回值的,也没用ref、out这样修饰的参数。如果有返回值那么返回的将是什么值呢?答案就是,委托链中最后一个订阅执行的返回值,ref、out也是一样。那么如果想获取所有订阅执行的返回结果我们就必须使用像异常处理第一种方式的方法,就是使用GetInvocationList()方法依次执行,并存储返回值。
Event
Event关键字,为我们‘无形中’解决了Delegate两个潜在的问题:
1.封装订阅,屏蔽‘=’运算符
上文提到,如果将+=、-=误写成=那么将引入一个bug,但是如果使用Event关键字则=运算符将无法使用。
2.封装发布,无法在外部触发事件
因为Delegate声明为public方法,那么触发委托就可以轻而易举在外部完成,任何拥有thermostat对象的类都可以诉说‘狼来了’的故事,即使温度从来就没有发生过变化。Event关键字确保了只有包容类才能调用向所有订阅者发出通知的委托。想在外部说‘狼来了’(调用),就变成了不可能的事,从而保证了封装性。
用Event代替Delegate提供接口的代码如下:
//定义委托 public delegate void TemperatureChangeHanlder(float newTemp); public event TemperatureChangeHanlder OnTemperatureChange; //private TemperatureChangeHanlder _onTemperatureChange; ////暴露给订阅者 //public TemperatureChangeHanlder OnTemperatureChange //{ // get // { // return _onTemperatureChange; // } // set // { // _onTemperatureChange = value; // } //}
但是声明Event是需要遵循规范的EventHanlder(object sender,EventArgs args)。当然微软办事都是很贴心的,整个一个阳光小暖男的角色,自然为我们准备好了默认的委托,如果需要特定的Sender-EventArgs模式可以使用EventHanlder<T>,不必从新声明委托。最后完整的代码变成了:
class Program { static void Main(string[] args) { Thermostat thermostat = new Thermostat(); Condition heater = new Heater(16); Condition cooler = new Cooler(26); //注册 thermostat.OnTemperatureChange += heater.OnConditionTemperatureChange; thermostat.OnTemperatureChange += cooler.OnConditionTemperatureChange; thermostat.CurrentTemperature = 20; Console.Read(); } } public class Thermostat { private float _currentTemperature; public float CurrentTemperature { get { return _currentTemperature; } set { if (value != _currentTemperature) { _currentTemperature = value; foreach (EventHandler<TemperatureArgs> item in OnTemperatureChange.GetInvocationList()) { try { item(this, new TemperatureArgs(value)); } catch (Exception) { // do something } } } } } //定义委托 //public delegate void TemperatureChangeHanlder(float newTemp); public event EventHandler<TemperatureArgs> OnTemperatureChange; //private TemperatureChangeHanlder _onTemperatureChange; ////暴露给订阅者 //public TemperatureChangeHanlder OnTemperatureChange //{ // get // { // return _onTemperatureChange; // } // set // { // _onTemperatureChange = value; // } //} } public class TemperatureArgs : EventArgs { public float ConditionTemperatureValue { get; set; } public TemperatureArgs(float value) : base() { ConditionTemperatureValue = value; } } public abstract class Condition { public float ConditionTemperature { get; set; } public Condition(float temp) { ConditionTemperature = temp; } public abstract void OnConditionTemperatureChange(object sender, TemperatureArgs newTemp); } class Heater : Condition { public Heater(float value) : base(value) { } //实现订阅的方法 public override void OnConditionTemperatureChange(object sender, TemperatureArgs newTemp) { if (newTemp.ConditionTemperatureValue > ConditionTemperature) { Console.WriteLine("Temperature is {0},heater will turn off.", newTemp.ConditionTemperatureValue); } else { Console.WriteLine("Temperature is {0},heater will turn on.", newTemp.ConditionTemperatureValue); } } } class Cooler : Condition { public Cooler(float value) : base(value) { } public override void OnConditionTemperatureChange(object sender, TemperatureArgs newTemp) { if (newTemp.ConditionTemperatureValue > ConditionTemperature) { Console.WriteLine("Temperature is {0},cooler will turn on.", newTemp.ConditionTemperatureValue); } else { Console.WriteLine("Temperature is {0},cooler will turn off.", newTemp.ConditionTemperatureValue); } } } class Program { static void Main(string[] args) { Thermostat thermostat = new Thermostat(); Heater heater = new Heater(16); Cooler cooler = new Cooler(26); thermostat.OnTemperatureChange += heater.OnHeaterTemperatureChange; thermostat.OnTemperatureChange += cooler.OnCoolerTemperatureChange; thermostat.OnTemperatureChange(20); Console.Read(); } } public class Thermostat { private float _currentTemperature; public float CurrentTemperature { get { return _currentTemperature; } set { if (value != _currentTemperature) _currentTemperature = value; } } public delegate void TemperatureChangeHanlder(float newTemp); private TemperatureChangeHanlder _onTemperatureChange; public TemperatureChangeHanlder OnTemperatureChange { get { return _onTemperatureChange; } set { _onTemperatureChange = value; } } } class Heater { public Heater(float value) { HeatTemperature = value; } public float HeatTemperature { get; set; } public void OnHeaterTemperatureChange(float newTemp) { if (newTemp > HeatTemperature) { Console.WriteLine("Temperature is {0},heater will turn off.", newTemp); } else { Console.WriteLine("Temperature is {0},heater will turn on.", newTemp); } } } class Cooler { public Cooler(float value) { CoolerTemperature = value; } public float CoolerTemperature { get; set; } public void OnCoolerTemperatureChange(float newTemp) { if (newTemp > CoolerTemperature) { Console.WriteLine("Temperature is {0},cooler will turn on.", newTemp); } else { Console.WriteLine("Temperature is {0},cooler will turn off.", newTemp); } } }