事件可以理解成封装性更好的委托,使得Publish-Subscribe(发布——订阅)模式更简洁且更不易出Bug。
若不使用事件,则可使用多播委托实现Publish-Subscribe(发布——订阅)模式。这里考虑一个温度控制的例子:
假设一个加热器(Heater)和一个冷却器(Cooler)连接到同一个温度计(Thermostat)。控制设备开关需要向它们通知温度变化。温度计(发布者)将温度变化发布给多个订阅者——也就是加热器和冷却器,加热器和冷却器根据温度器的变化而做出反应。
代码如下:
Thermostat.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
//温度计类
public class Thermostat
{
//定义一个委托
//来自Publisher的温度变化通知会同时被Cooler和Heater的实例(Subscriber)所接收。
public Action<float> OnTemperatureChange{get; set;}
private float _CurrentTemperature;
public float CurrentTemperature
{
get {return _CurrentTemperature;}
set
{
if(value!=_CurrentTemperature)
_CurrentTemperature = value;
OnTemperatureChange?.Invoke(value);//6.0这样写,防止出现空委托Bug
}
}
}
Heater.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static System.Console;
//加热器类
public class Heater
{
public float TemperatureThreshold { get; set; }
public Heater(float temperatureThreshold)
{
TemperatureThreshold = temperatureThreshold;
}
public void OnTemperatureChanged(float newTemperature)
{
if (newTemperature < TemperatureThreshold)
WriteLine("加热器开");
else
WriteLine("加热器关");
}
}
Cooler.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static System.Console;
public class Cooler
{
public float TemperatureThreshold { get; set; }
public Cooler(float temperatureThreshold)
{
TemperatureThreshold = temperatureThreshold;
}
public void OnTemperatureChanged(float newTemperature)
{
if (newTemperature > TemperatureThreshold)
WriteLine("冷却器开");
else
WriteLine("冷却器关");
}
}
进行测试:
Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static System.Console;
class Program
{
static void Main(string[] args)
{
Thermostat thermostat = new Thermostat();
Heater heater = new Heater(50);//加热阈值为50度
Cooler cooler = new Cooler(100);//冷却阈值为100度
thermostat.OnTemperatureChange += heater.OnTemperatureChanged;
thermostat.OnTemperatureChange += cooler.OnTemperatureChanged;
int currentTemperature;
while (true)
{
WriteLine("请输入当前温度: ");
if(int.TryParse(Console.ReadLine(), out currentTemperature))
thermostat.CurrentTemperature = currentTemperature;
else
{
break;
}
}
ReadKey();
}
}
结果如下:
加热器和冷却器成功的根据温度器(测量当前温度值)的变化而做出反应。但委托结构中存在的缺陷很可能造成程序员在不经意中引入Bug,问题主要和封装有关,无论时间的订阅还是发布,都不能得到充分控制。具体问题如下::
- 对订阅的封装
错误使用赋值操作符"=”而不是“+=”, 这是一个十分容易犯的错误。其结果就是当代码将cooler.OnTemperatureChanged赋给OnTemperatureChange时,heater.OnTemperatureChanged会被清除,因为一个全新的委托链代替了之前已经存在的委托链。所以最好的解决方案是根本不要为包容类外部的对象提供对赋值操作符的支持。而event关键词(定义事件类型)的作用就是提供额外的封装,避免不小心取消其他订阅者。 - 对发布的封装
委托和事件的第二个重要区别在于,事件类型确保了只有包容类本身才能触发事件通知。而委托可从包容类的外部触发通知。
在Program.cs中添加代码
thermostat.OnTemperatureChange(57);
即使thermostat的CurrentTemperature没有变化,也可以发现Program简单的成功调用了
OnTemperatureChange委托,而Thermostat应禁止其他任何类调用OnTemperatureChange委托。
现在改用事件来实现Publish-Subscribe(发布——订阅)模式,仍然用该案例:
更改代码如下:
Thermostat.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
//温度计类
public class Thermostat
{
//定义一个委托
//来自Publisher的温度变化通知会同时被Cooler和Heater的实例(Subscriber)所接收。
//public Action<float> OnTemperatureChange { get; set; }
public class TemperatureArgs : System.EventArgs
{
public TemperatureArgs(float newTemperature)
{
NewTemperature = newTemperature;
}
public float _NewTemperature;
public float NewTemperature
{
get { return _NewTemperature; }
set { _NewTemperature = value; }
}
}
public delegate void TemperatureChangeHandler(object sender, TemperatureArgs newTemperature);
public event TemperatureChangeHandler OnTemperatureChange;
private float _CurrentTemperature;
public float CurrentTemperature
{
get { return _CurrentTemperature; }
set
{
if (value != _CurrentTemperature)
_CurrentTemperature = value;
//OnTemperatureChange?.Invoke(value);//6.0这样写,防止出现空委托Bug
OnTemperatureChange?.Invoke(this, new TemperatureArgs(value));
}
}
}
Heater.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static System.Console;
public class Heater
{
public float TemperatureThreshold { get; set; }
public Heater(float temperatureThreshold)
{
TemperatureThreshold = temperatureThreshold;
}
public void OnTemperatureChanged(object sender, Thermostat.TemperatureArgs temperature)
{
if (temperature.NewTemperature < TemperatureThreshold)
WriteLine("加热器开");
else
WriteLine("加热器关");
}
}
Cooler.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static System.Console;
public class Cooler
{
public float TemperatureThreshold { get; set; }
public Cooler(float temperatureThreshold)
{
TemperatureThreshold = temperatureThreshold;
}
public void OnTemperatureChanged(object sender, Thermostat.TemperatureArgs temperatureArgs)
{
if (temperatureArgs.NewTemperature > TemperatureThreshold)
WriteLine("冷却器开");
else
WriteLine("冷却器关");
}
}
Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static System.Console;
class Program
{
static void Main(string[] args)
{
Thermostat thermostat = new Thermostat();
Heater heater = new Heater(50);//加热阈值为50度
Cooler cooler = new Cooler(100);//冷却阈值为100度
thermostat.OnTemperatureChange += heater.OnTemperatureChanged;
thermostat.OnTemperatureChange += cooler.OnTemperatureChanged;
int currentTemperature;
while (true)
{
WriteLine("请输入当前温度: ");
if (int.TryParse(Console.ReadLine(), out currentTemperature))
thermostat.CurrentTemperature = currentTemperature;
else
{
break;
}
}
ReadKey();
}
}
运动结果:
注意:事件避免了由委托封装性问题导致的Bug:
另外,事件也确保了只有包容类才能触发事件通知,而无法从包容类的外部触发通知。