C#8.0本质论第十四章–事件
委托本身是一个更大的模式(Pattern)的基本单位,称为Publish-Subscribe(发布-订阅)或Observer(观察者)。
14.1使用多播委托实现Publish-Subscribe模式
14.1.1定义订阅者方法
public class Cooler
{public Cooler(float temperature){Temperature = temperature;}// Cooler is activated when ambient temperature// is higher than thispublic float Temperature { get; set; }// Notifies that the temperature changed on this instancepublic 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 thispublic float Temperature { get; set; }// Notifies that the temperature changed on this instancepublic void OnTemperatureChanged(float newTemperature){if(newTemperature < Temperature){System.Console.WriteLine("Heater: On");}else{System.Console.WriteLine("Heater: Off");}}
}
14.1.2定义发布者
public class Thermostat
{// Define the event publisher (initially without the sender)public Action<float>? OnTemperatureChange { get; set; }public float CurrentTemperature { get; set; }
}
14.1.3连接发布者和订阅者
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;}
}
14.1.4调用委托
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;
}
14.1.5检查空值
public class Thermostat
{// Define the event publisherpublic Action<float>? 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 subscribersOnTemperatureChange?.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<float>? localOnChange=OnTemperatureChange;if(localOnChange!=null){localOnChange(value);}}}}
不是一上来就检查空值,而是先将OnTemperatureChange赋给第二个委托局部变量localOnChange。这个简单的修改可确保在检查空值和发送通知之间,如一个不同的线程移除了所有OnTemperatureChange订阅者,将不会引发NullReferenceException异常。
既然委托是引用类型,肯定有人会感到疑惑:为什么赋值给一个局部变量,再用那个局部变量就能保证null检查的线程安全性?因为localOnChange指向的位置就是OnTemperatureChange指向的位置,所以很自然的结论是:OnTemperatureChange中发生的任何变化都将在localOnChange中反映。
但实情并非如此。事实上,对OnTemperatureChange-=< subscriber >的任何调用都不会从OnTemperatureChange删除一个委托,而使它包含的委托比之前少一个。相反,该调用会赋值一个全新的多播委托,原始委托不受任何影响。
虽然这样可以防范调用空委托,但不能防范所有可能的竞态条件。例如一个线程拷贝委托,另一个将委托重置为null,然后原始线程调用委托之前的值,向一个已经不在列表中的订阅者发生通知。
14.1.6委托操作符
使用赋值操作符会清除之前的所有订阅者,并允许用新订阅者替换。这是委托很容易让人犯错的地方,因为在本来应该使用“+=”操作符的时候,很容易会错误地写成“=”。
无论“+”“-”还是它们的复合版本,内部都使用静态方法System.Delegate.Combine()和System.Delegate.Remove()来分别实现。
14.1.7顺序调用
MulticastDelegate类事实上维护者一个Delegate对象链表。调用多播委托时,链表中的委托实例被顺序调用。通常,委托按它们添加的顺序调用,但CLI并未对此做出规定,而且顺序可能被覆盖,所以程序员不应依赖特定调用顺序。
14.1.8错误处理
一个订阅者抛出异常,链中的后续订阅者就收不到通知。
为避免该问题,必须手动遍历订阅者列表,并单独调用它们。可以从委托的GetInvocationList()方法获取一份订阅者列表。
14.1.9方法返回值和传引用
还有一种情况需要遍历委托调用列表而非直接调用一个委托。这种情况的委托要么不返回void,要么具有ref或out参数。
14.2理解事件
14.2.1事件的作用
使用事件的好处是,只有直接持有一个事件对象的类可以调用这个事件对象,其他的类只能使用+=或-=向这个事件对象添加或删除对事件的订阅。(我试了下,自己类是可以用=覆盖的)
event关键字的作用就是提供额外的封装。
所以事件与委托的区别就是:1.只有直接持有一个事件对象的类可以调用这个事件对象。2.其他的类只能使用+=或-=向这个事件对象添加或删除对事件的订阅。
14.2.2声明事件
C#用event关键字声明事件,虽然看起来像是一个字段修饰符,但event定义了新的成员类型。
public class TemperatureArgs : System.EventArgs{public TemperatureArgs(float newTemperature){NewTemperature = newTemperature;}public float NewTemperature { get; set; }}// Define the event publisherpublic event EventHandler<TemperatureArgs> OnTemperatureChange = delegate { };
普通委托另一个潜在缺陷在于很容易忘记在调用委托之前检查null值。幸好,在声明事件时可以赋值一个空白委托delegate { },就可引发事件而不必检查是否有任何订阅者。
14.2.3编码规范
14.2.4泛型和委托
事件的内部机制:C#编译器获取带有event关键字修饰符的public委托变量,在内部将委托声明为private,并添加了两个方法和两个特殊的事件块。简单地说,event关键字是编译器生成适合封装逻辑的C#快捷方式。
public class Thermostat
// ...public event EventHandler<TemperatureArgs>? OnTemperatureChange;
}
C#编译器遇到event关键字后生成的CIL代码等价于下面代码
public class Thermostat
// ...// Declaring the delegate field to save the // list of subscribersprivate EventHandler<TemperatureArgs>? _OnTemperatureChange;public void add_OnTemperatureChange(EventHandler<TemperatureArgs> handler){System.Delegate.Combine(_OnTemperatureChange, handler);}public void remove_OnTemperatureChange(EventHandler<TemperatureArgs> handler){System.Delegate.Remove(_OnTemperatureChange, handler);}#if ConceptualEquivalentCodepublic event EventHandler<TemperatureArgs> OnTemperatureChange{//Would cause a compiler erroradd{add_OnTemperatureChange(value);}//Would cause a compiler errorremove{remove_OnTemperatureChange(value);}}
14.2.5实现自定义事件
C#允许添加自定义的add和remove块。
public class Thermostat
{public class TemperatureArgs : System.EventArgs// ...// Define the event publisherpublic event EventHandler<TemperatureArgs> OnTemperatureChange{add{_OnTemperatureChange = (EventHandler<TemperatureArgs>)System.Delegate.Combine(value, _OnTemperatureChange);}remove{_OnTemperatureChange = (EventHandler<TemperatureArgs>?)System.Delegate.Remove(_OnTemperatureChange, value);}}protected EventHandler<TemperatureArgs>? _OnTemperatureChange;public float CurrentTemperature// ...private float _CurrentTemperature;
}