Skip to content

C# - 事件

事件是 C# 中的一种机制,它允许一个类或对象(发布者,publisher)在发生某些感兴趣的事情时通知其他类或对象(订阅者,subscribers)。这基于发布-订阅模式(Publisher-Subscriber pattern),对于构建松散耦合和响应式应用程序至关重要。

常见示例包括 UI 中的用户操作(按钮点击)、后台任务完成或系统状态变化(温度传感器超出阈值)。

C# 中的现代事件模式涉及三个主要部分:

  • 委托(Delegate):这是一种定义事件处理方法签名的类型。标准约定是使用 System.EventHandler 或泛型 System.EventHandler<TEventArgs>。
  • 事件数据:一个继承自 System.EventArgs 的类。它用于将有关事件的数据从发布者传递给订阅者。
  • 事件本身:发布者类的一个成员,使用 event 关键字声明。此关键字确保订阅者只能添加 (+=) 或移除 (-=) 处理程序,而不能直接调用事件。

一个现代事件示例:Boiler(锅炉)

Section titled “一个现代事件示例:Boiler(锅炉)”

让我们创建一个 Boiler 类,当其温度达到危险高位时触发一个事件。我们将遵循现代 C# 最佳实践。

// 此类携带着有关事件的信息。
public class BoilerEventArgs : EventArgs
{
public double Temperature { get; }
public string Message { get; }
public BoilerEventArgs(double temperature, string message)
{
Temperature = temperature;
Message = message;
}
}
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);
}
}
// 订阅者类
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°C
LOG: 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 等事件,以通知主应用程序线程结果已准备就绪。
  • 内存泄漏:这是最常见的问题。如果一个生命周期长的订阅者(例如,一个静态类)订阅了一个生命周期短的发布者事件,那么发布者将不会被垃圾回收,因为订阅者持有对它的引用。当不再需要订阅者时,务必从事件中取消订阅(-=),尤其是在实现 IDisposable 的类中。
  • 线程安全:事件处理程序在触发事件的同一个线程上执行。如果后台线程触发了一个更新 UI 的事件,您必须将 UI 更新调度回 UI 线程。这是 GUI 编程中的一个关键概念。
  • 使用 EventHandler<T>:避免创建自定义委托类型。为了保持一致性,请坚持使用标准的 EventHandler(用于没有数据的事件)和 EventHandler<TEventArgs>。
  • 使事件数据不可变:EventArgs 类上的属性应该是 readonly({ get; }),以防止订阅者修改事件数据,这可能对其他订阅者造成不可预测的副作用。