C# - 事件
C# - 事件
Section titled “C# - 事件”事件是 C# 中的一种机制,它允许一个类或对象(发布者,publisher)在发生某些感兴趣的事情时通知其他类或对象(订阅者,subscribers)。这基于发布-订阅模式(Publisher-Subscriber pattern),对于构建松散耦合和响应式应用程序至关重要。
常见示例包括 UI 中的用户操作(按钮点击)、后台任务完成或系统状态变化(温度传感器超出阈值)。
事件的核心组件
Section titled “事件的核心组件”C# 中的现代事件模式涉及三个主要部分:
- 委托(Delegate):这是一种定义事件处理方法签名的类型。标准约定是使用
System.EventHandler或泛型System.EventHandler<TEventArgs>。 - 事件数据:一个继承自
System.EventArgs的类。它用于将有关事件的数据从发布者传递给订阅者。 - 事件本身:发布者类的一个成员,使用
event关键字声明。此关键字确保订阅者只能添加 (+=) 或移除 (-=) 处理程序,而不能直接调用事件。
一个现代事件示例:Boiler(锅炉)
Section titled “一个现代事件示例:Boiler(锅炉)”让我们创建一个 Boiler 类,当其温度达到危险高位时触发一个事件。我们将遵循现代 C# 最佳实践。
步骤 1:定义事件数据类
Section titled “步骤 1:定义事件数据类”// 此类携带着有关事件的信息。public class BoilerEventArgs : EventArgs{ public double Temperature { get; } public string Message { get; }
public BoilerEventArgs(double temperature, string message) { Temperature = temperature; Message = message; }}步骤 2:创建发布者类
Section titled “步骤 2:创建发布者类”public class Boiler{ // 1. 使用泛型 EventHandler<T> 委托定义事件。 // 这表示此事件的任何处理程序都必须接受 BoilerEventArgs。 public event EventHandler<BoilerEventArgs>? BoilerAlert;
private double _currentTemperature;
public void SetTemperature(double temp) { _currentTemperature = temp; Console.WriteLine($"Boiler temperature set to: {_currentTemperature}°C");
// 2. 检查条件并在必要时触发事件。 if (_currentTemperature > 95.0) { // 创建事件参数 var args = new BoilerEventArgs(_currentTemperature, "DANGER: Boiler is overheating!");
// 3. 触发事件 OnBoilerAlert(args); } }
// 4. 一个用于触发事件的 protected virtual 方法(最佳实践)。 // 这允许派生类自定义事件触发行为。 protected virtual void OnBoilerAlert(BoilerEventArgs e) { // null 条件运算符 (?.) 确保我们仅在 // 存在订阅者时才调用事件,从而防止 NullReferenceException。 BoilerAlert?.Invoke(this, e); }}步骤 3:创建订阅者并处理事件
Section titled “步骤 3:创建订阅者并处理事件”// 订阅者类public class BoilerLogger{ // 事件处理方法。其签名必须与 EventHandler<BoilerEventArgs> 委托匹配。 public void LogAlert(object? sender, BoilerEventArgs e) { Console.WriteLine($"LOG: Alert received. Temp: {e.Temperature}°C. Message: {e.Message}"); }}
// 另一个订阅者public class SafetySystem{ public void ShutdownBoiler(object? sender, BoilerEventArgs e) { Console.WriteLine($"SAFETY: Shutting down boiler immediately! Temp was {e.Temperature}°C."); }}
// --- 整合所有部分 ---
// 1. 创建发布者和订阅者的实例var boiler = new Boiler();var logger = new BoilerLogger();var safety = new SafetySystem();
// 2. 订阅事件。这些方法现在是事件处理程序。boiler.BoilerAlert += logger.LogAlert;boiler.BoilerAlert += safety.ShutdownBoiler;
// 您也可以使用 Lambda 表达式订阅,以便编写简洁的处理程序boiler.BoilerAlert += (sender, e) => { Console.WriteLine($"EMAIL: Notifying administrator about temperature {e.Temperature}°C.");};
// 3. 触发将导致事件被触发的操作Console.WriteLine("--- Normal Operation ---");boiler.SetTemperature(80);
Console.WriteLine("\n--- DANGER --- ");boiler.SetTemperature(98);
// 取消订阅:// boiler.BoilerAlert -= logger.LogAlert;编译并执行上述代码后,会产生以下结果:
--- Normal Operation ---Boiler temperature set to: 80°C
--- DANGER ---Boiler temperature set to: 98°CLOG: Alert received. Temp: 98°C. Message: DANGER: Boiler is overheating!SAFETY: Shutting down boiler immediately! Temp was 98°C.EMAIL: Notifying administrator about temperature 98°C.- GUI 框架:在 WPF、MAUI 或 WinForms 中,像
Button这样的 UI 元素会发布Click事件。您的代码订阅此事件以执行操作。 - ASP.NET Core:应用程序生命周期中存在
ApplicationStarted等事件,服务可以挂接到这些事件。 - 异步编程:后台服务可以触发
WorkCompleted等事件,以通知主应用程序线程结果已准备就绪。
常见陷阱和最佳实践
Section titled “常见陷阱和最佳实践”- 内存泄漏:这是最常见的问题。如果一个生命周期长的订阅者(例如,一个静态类)订阅了一个生命周期短的发布者事件,那么发布者将不会被垃圾回收,因为订阅者持有对它的引用。当不再需要订阅者时,务必从事件中取消订阅(
-=),尤其是在实现IDisposable的类中。 - 线程安全:事件处理程序在触发事件的同一个线程上执行。如果后台线程触发了一个更新 UI 的事件,您必须将 UI 更新调度回 UI 线程。这是 GUI 编程中的一个关键概念。
- 使用
EventHandler<T>:避免创建自定义委托类型。为了保持一致性,请坚持使用标准的EventHandler(用于没有数据的事件)和EventHandler<TEventArgs>。 - 使事件数据不可变:
EventArgs类上的属性应该是readonly({ get; }),以防止订阅者修改事件数据,这可能对其他订阅者造成不可预测的副作用。