事件的记忆碎片
前言
委托类型的实例是存储着一个方法,并通过委托来调用那个方法,但是委托还有其他的用途。
先讲一个模式:publish-subscribe(订阅-发布)
它是应对这样的一个场景情形:就是把单一事件的通知广播给多个订阅者。
这句话通俗一点讲的话就是:
现在有方法A、B、C、D、E,自己想调用这五个方法中的全部或者部分方法。但是又不想一个一个显式的去调用,因为如果方法很多的话就会形成一个代码的堆砌,不够简洁,时间一长也不好维护。这时候有一个想法就是能有一个“方法F”来收集自己想要调用的方法就好了,最后自己只要调用“方法F”就可以调用所有自己想要调用的方法。
到这边有人可能就感觉不是很自然,感觉有点陌生,没有关系,我也是和你一样很陌生,本文就是帮助你对publish-subscribe(订阅-发布)这个模式的熟悉并且完全掌握。
好的,publish-subscribe(订阅-发布)先讲到这边。本文的第一句话就提到委托还有其他的用途。具体是指:一个委托变量可以引用一系列委托,在这一系列委托中,每一个委托都会顺序指向一个后续的委托,从而形成一个委托链。只要调用这个委托的方法对象,在这个委托链上的所有方法就会按照委托链的顺序一一执行。我们在这边可以做一个猜想:委托变量可以调用多个方法,是不是“方法F”就是用委托变量来实现的呢? 其实答案就是这样。
具体的场景描述:
来考虑一个温度控制的例子。一个加热器和一个冷却器连接到同一个自动调温器。为了控制加热器和冷却器的打开和关闭,要向他们通知温度的变化。自动调温器将温度的变化发布给多个订阅者----也就是加热器和冷却器。
一、定义订阅者的方法
public class Cooler
{
public Cooler(float temperature)
{
Temperature = temperature;
}
public float Temperature { get; set; }
public void OnTemperatureChanged(float newTemperature)
{
if (newTemperature > Temperature)
{
Console.WriteLine("Cooler:ON");
}
else
{
Console.WriteLine("Cooler:Off");
}
}
}
public class Heater
{
public Heater(float temperature)
{
Temperature = temperature;
}
public float Temperature { get; set; }
public void OnTemperatureChanged(float newTemperature)
{
if (newTemperature < Temperature)
{
Console.WriteLine("Heater:ON");
}
else
{
Console.WriteLine("Heater:Off");
}
}
}
这两个类几乎完全一致,两个类都提供了一个OnTemperatureChanged方法,调用OnTemperatureChanged就是为了向Heater和Cooler指出温度变化,并决定是否让设备启动。在这里,两个OnTemperatureChanged方法都是订阅者方法。作为订阅者方法很重要的是它们的参数和返回值类型必须和自动调温器中的委托匹配。
二、定义发布者
Thermostat类负责向heater和cooler对象实例报告温度的变化。
public class Thermostat
{
public delegate void TemperarureChangeHandler(float newTemperature);
public TemperarureChangeHandler OnTemperatureChange
{ get; set; }
public float CurrentTemperature//接受当前的温度
{ get; set; }
}
这个类的第一个成员是TemperarureChangeHandler 委托。定义了订阅者的方法类型,就是说在Heater和Cooler类中的OnTemperatureChanged成员方法和TemperarureChangeHandler委托是匹配的。OnTemperatureChange成员属性是TemperarureChangeHandler委托类型的,将会用来存储着订阅者列表。最后一个CurrentTemperature属性是用来接收当前的温度的。
三、连接发布者和订阅者
public class Program
{
Thermostat thermostat = new Thermostat();
Heater heater = new Heater(60);
Cooler cooler = new Cooler(80);
string temerature;
thermostat.OnTemperatureChange += heater.OnTemperatureChanged;
thermostat.OnTemperatureChange += cooler.OnTemperatureChanged;
Console.Write("Enter temperature:");
temerature = Console.ReadLine();
thermostat.CurrentTemperature = int.Parse(temerature);
}
注意上述代码使用+=运算符来直接赋值,向OnTemperatureChange委托注册了两个订阅者。但是目前还没有写任何代码将温度变化发布给订阅者。
四、调用委托,向订阅者通知温度的变化
public class Thermostat
{
…
…
public float CurrentTemperature//当CurrentTemperature属性每次变化的时候调用委托来向订阅者通知温度的变化
{
get
{
return _CurrentTemperature;
}
set
{
if (value != CurrentTemperature)
{
_CurrentTemperature=value;
OnTemperatureChange(value);
}
}
}
private float _CurrentTemperature;
}
成功了成功了,现在对CurrentTemperature赋值包含了一些特殊的逻辑,可以向订阅者通知CurrentTemperature发生了变化。为了向所有的订阅者发出通知,只需执行一个简单的C# 语句即:OnTemperatureChange(value); 这个语句将温度的变化发给Cooler和Heater的对象,只需执行一个调用,即可向多个订阅者发出通知。这里的实现就是基于一个委托变量可以保存一个委托链。
五、在调用委托之前必须检查委托对象是否为空
public class Thermostat
{
…
…
public float CurrentTemperature//当CurrentTemperature属性每次变化的时候调用委托检查空值
{
get
{
return _CurrentTemperature;
}
set
{
if (value != CurrentTemperature)
{
_CurrentTemperature = value;
TemperarureChangeHandler localOnChange = OnTemperatureChange;//OnTemperatureChange中发生的任何改变都会在localOnChange中反映出来
if (localOnChange != null)
{
localOnChange(value);
}
}
}
}
private float _CurrentTemperature;
}
在这里,并不是一开始就检查空值,而是首先是将OnTemperatureChange赋值给 localOnChange因为OnTemperatureChange 的订阅者被不是同一个线程的方法移除时候,那么不会触发NullReferenceException异常。
六、处理来自订阅者的异常
public class Thermostat
{
…
…
public float CurrentTemperature//当CurrentTemperature属性每次变化的时候调用委托并处理来自订阅者的异常
{
get
{
return _CurrentTemperature;
}
set
{
if (value != CurrentTemperature)
{
_CurrentTemperature = value;
//OnTemperatureChange中发生的任何改变都会在localOnChange中反映出来TemperarureChangeHandler localOnChange = OnTemperatureChange;
if (localOnChange != null)
{
//防止因为订阅者的异常导致在异常后面的订阅者不能接收到发布事件foreach (TemperarureChangeHandler handler in localOnChange.GetInvocationList())
{
try
{
handler(value);
}
catch (Exception exception)
{
Console.WriteLine(exception.Message);
}
}
}
}
}
}
private float _CurrentTemperature;
}
委托链是将多个方法串联在一起的,假如一个委托链上有A、B、C三个注册的方法,但是假如A中出现异常,B和C是不会继续执行的,那么解决的办法就是用foreach 上面的代码就是解决方案。
七、事件的出现
讲了这么多的还是没有讲到事件,哈哈,是不是被耍了啊。不要急,一会就会讲到事件了啊。
我们看上面的委托处理还是很不错的,不过有两点不是很好
1、 在Main方法中给委托注册事件
thermostat.OnTemperatureChange += heater.OnTemperatureChanged;
thermostat.OnTemperatureChange += cooler.OnTemperatureChanged;
假设我们不小心的把+=写成了=,就会出现我们不想要的结果,就会在thermostat.OnTemperatureChange 中保存最后一个赋值的方法,而之前的都会被重写,容易出错。
2、 委托可以在包容类之外被执行调用
还是在Main方法中可以加上这句话:
thermostat.OnTemperatureChange(56);
加上这句话会导致的结果是:即使thermostat的CurrentTemperature没有发生变化,OnTemperatureChange也能被调用。因此thermostat订阅者有可能被通知说温度变化了,而实际上CurrentTemperature的温度并没有变化。
C# 用event 关键字解决上面的两个问题。代码:
public class Thermostat
{
public class TemperatureArgs : System.EventArgs
{
public TemperatureArgs(float newTemperature)
{
NewTemperature = newTemperature;
}
public float NewTemperature
{ get; set; }
}
//public delegate void TemperarureChangeHandler(float newTemperature);
public delegate void TemperarureChangeHandler(object sender,TemperatureArgs newTemperature);
public event TemperarureChangeHandler OnTemperatureChange = delegate { };
public float CurrentTemperature//当CurrentTemperature属性每次变化的时候调用委托并处理来自订阅者的异常
{
get
{
return _CurrentTemperature;
}
set
{
if (value != CurrentTemperature)
{
_CurrentTemperature = value;
TemperarureChangeHandler localOnChange = OnTemperatureChange;//OnTemperatureChange中发生的任何改变都会在localOnChange中反映出来
if (localOnChange != null)
{
foreach (TemperarureChangeHandler handler in localOnChange.GetInvocationList())//防止因为订阅者的异常导致在异常后面的订阅者不能接收到发布事件
{
try
{
handler(this,new TemperatureArgs(value));
}
catch (Exception exception)
{
Console.WriteLine(exception.Message);
}
}
}
}
}
}
private float _CurrentTemperature;
}
有了这样的声明,在加上event关键字后,提供了我们需要的全部封装。首先会禁止使用赋值运算符,然后只有包容类才能调用向所有的订阅者发出通知的委托。