委托本身是一个更大的模式(Pattern)的基本单位,称为Publish-Subscribe(发布-订阅)或Observer(观察者)。
public class Cooler
{
public Cooler(float temperature)
{
Temperature = temperature;
}
// Cooler is activated when ambient temperature
// is higher than this
public float Temperature { get; set; }
// Notifies that the temperature changed on this instance
public void OnTemperatureChanged(float newTemperature)
{
if(newTemperature > Temperature)
{
System.Console.WriteLine("Cooler: On");
}
else
{
System.Console.WriteLine("Cooler: Off");
}
}
}
public class Heater
{
public Heater(float temperature)
{
Temperature = temperature;
}
// Heater is activated when ambient temperature
// is lower than this
public float Temperature { get; set; }
// Notifies that the temperature changed on this instance
public void OnTemperatureChanged(float newTemperature)
{
if(newTemperature < Temperature)
{
System.Console.WriteLine("Heater: On");
}
else
{
System.Console.WriteLine("Heater: Off");
}
}
}
public class Thermostat
{
// Define the event publisher (initially without the sender)
public Action? OnTemperatureChange { get; set; }
public float CurrentTemperature { get; set; }
}
public class Program
{
public static void Main()
{
Thermostat thermostat = new();
Heater heater = new(60);
Cooler cooler = new(80);
thermostat.OnTemperatureChange +=
heater.OnTemperatureChanged;
thermostat.OnTemperatureChange +=
cooler.OnTemperatureChanged;
Console.Write("Enter temperature: ");
string? temperature = Console.ReadLine();
if (!int.TryParse(temperature, out int currentTemperature))
{
Console.WriteLine($"'{temperature}' is not a valid integer.");
return;
}
thermostat.CurrentTemperature = currentTemperature;
}
}
public class Thermostat
{
// ...
public float CurrentTemperature
{
get { return _CurrentTemperature; }
set
{
if (value != CurrentTemperature)
{
_CurrentTemperature = value;
// Call subscribers
// Incomplete, check for null needed
// ...
OnTemperatureChange(value);
// ...
}
}
}
private float _CurrentTemperature;
}
public class Thermostat
{
// Define the event publisher
public Action? OnTemperatureChange { get; set; }
public float CurrentTemperature
{
get { return _CurrentTemperature; }
set
{
if(value != CurrentTemperature)
{
_CurrentTemperature = value;
// If there are any subscribers,
// notify them of changes in
// temperature by invoking said subscribers
OnTemperatureChange?.Invoke(value); // C# 6.0
}
}
}
private float _CurrentTemperature;
}
注意:OnTemperatureChange?.Invoke(value);
空条件操作符的优点在于,它采用特殊逻辑防范在执行空检查后订阅者调用一个过时处理程序(空检查后有变)导致委托再度为空。
在C#6.0之前不存在这种特殊的、不会被干扰的空检查逻辑。老版本实现稍微麻烦一点。
public float CurrentTemperature
{
get { return _CurrentTemperature; }
set
{
if(value != CurrentTemperature)
{
_CurrentTemperature = value;
Action? localOnChange=OnTemperatureChange;
if(localOnChange!=null)
{
localOnChange(value);
}
}
}
}
不是一上来就检查空值,而是先将OnTemperatureChange赋给第二个委托局部变量localOnChange。这个简单的修改可确保在检查空值和发送通知之间,如一个不同的线程移除了所有OnTemperatureChange订阅者,将不会引发NullReferenceException异常。
既然委托是引用类型,肯定有人会感到疑惑:为什么赋值给一个局部变量,再用那个局部变量就能保证null检查的线程安全性?因为localOnChange指向的位置就是OnTemperatureChange指向的位置,所以很自然的结论是:OnTemperatureChange中发生的任何变化都将在localOnChange中反映。
但实情并非如此。事实上,对OnTemperatureChange-=< subscriber >的任何调用都不会从OnTemperatureChange删除一个委托,而使它包含的委托比之前少一个。相反,该调用会赋值一个全新的多播委托,原始委托不受任何影响。
虽然这样可以防范调用空委托,但不能防范所有可能的竞态条件。例如一个线程拷贝委托,另一个将委托重置为null,然后原始线程调用委托之前的值,向一个已经不在列表中的订阅者发生通知。
使用赋值操作符会清除之前的所有订阅者,并允许用新订阅者替换。这是委托很容易让人犯错的地方,因为在本来应该使用“+=”操作符的时候,很容易会错误地写成“=”。
无论“+”“-”还是它们的复合版本,内部都使用静态方法System.Delegate.Combine()和System.Delegate.Remove()来分别实现。
MulticastDelegate类事实上维护者一个Delegate对象链表。调用多播委托时,链表中的委托实例被顺序调用。通常,委托按它们添加的顺序调用,但CLI并未对此做出规定,而且顺序可能被覆盖,所以程序员不应依赖特定调用顺序。
一个订阅者抛出异常,链中的后续订阅者就收不到通知。
为避免该问题,必须手动遍历订阅者列表,并单独调用它们。可以从委托的GetInvocationList()方法获取一份订阅者列表。
还有一种情况需要遍历委托调用列表而非直接调用一个委托。这种情况的委托要么不返回void,要么具有ref或out参数。
使用事件的好处是,只有直接持有一个事件对象的类可以调用这个事件对象,其他的类只能使用+=或-=向这个事件对象添加或删除对事件的订阅。(我试了下,自己类是可以用=覆盖的)
event关键字的作用就是提供额外的封装。
所以事件与委托的区别就是:1.只有直接持有一个事件对象的类可以调用这个事件对象。2.其他的类只能使用+=或-=向这个事件对象添加或删除对事件的订阅。
C#用event关键字声明事件,虽然看起来像是一个字段修饰符,但event定义了新的成员类型。
public class TemperatureArgs : System.EventArgs
{
public TemperatureArgs(float newTemperature)
{
NewTemperature = newTemperature;
}
public float NewTemperature { get; set; }
}
// Define the event publisher
public event EventHandler OnTemperatureChange = delegate { };
普通委托另一个潜在缺陷在于很容易忘记在调用委托之前检查null值。幸好,在声明事件时可以赋值一个空白委托delegate { },就可引发事件而不必检查是否有任何订阅者。
事件的内部机制:C#编译器获取带有event关键字修饰符的public委托变量,在内部将委托声明为private,并添加了两个方法和两个特殊的事件块。简单地说,event关键字是编译器生成适合封装逻辑的C#快捷方式。
public class Thermostat
// ...
public event EventHandler? OnTemperatureChange;
}
C#编译器遇到event关键字后生成的CIL代码等价于下面代码
public class Thermostat
// ...
// Declaring the delegate field to save the
// list of subscribers
private EventHandler? _OnTemperatureChange;
public void add_OnTemperatureChange(
EventHandler handler)
{
System.Delegate.Combine(_OnTemperatureChange, handler);
}
public void remove_OnTemperatureChange(
EventHandler handler)
{
System.Delegate.Remove(_OnTemperatureChange, handler);
}
#if ConceptualEquivalentCode
public event EventHandler OnTemperatureChange
{
//Would cause a compiler error
add
{
add_OnTemperatureChange(value);
}
//Would cause a compiler error
remove
{
remove_OnTemperatureChange(value);
}
}
C#允许添加自定义的add和remove块。
public class Thermostat
{
public class TemperatureArgs : System.EventArgs
// ...
// Define the event publisher
public event EventHandler OnTemperatureChange
{
add
{
_OnTemperatureChange =
(EventHandler)
System.Delegate.Combine(value, _OnTemperatureChange);
}
remove
{
_OnTemperatureChange =
(EventHandler?)
System.Delegate.Remove(_OnTemperatureChange, value);
}
}
protected EventHandler? _OnTemperatureChange;
public float CurrentTemperature
// ...
private float _CurrentTemperature;
}