一、从一个生活中的场景说起
想象一下,你订阅了一份天气短信服务。气象局(发布者)的核心工作就是监测天气变化。一旦温度、湿度或气压发生变化,它不需要知道具体是谁关心这个信息,只需要“喊一嗓子”:“喂,数据变啦!”。而你(订阅者)在订阅时,就相当于告诉气象局:“这是我的手机号,有变化就发给我”。之后,每当数据更新,你就能自动收到短信。
这个过程,就是软件设计中经典的“观察者模式”。在C#的世界里,实现这种“喊一嗓子”和“留下联系方式”的机制,靠的就是“委托”和“事件”这两位黄金搭档。它们让发布者和订阅者之间既紧密联系,又相互独立,完美解耦。
二、委托:指向方法的“电话号码”
在C#里,委托(Delegate)本质上是一种类型,它代表了对方法的引用。你可以把它理解成一个“方法指针”或者“方法的电话号码簿”。它定义了“可以给什么样的人打电话”——即方法的签名(参数和返回类型)。
技术栈:C# / .NET 6+ 控制台应用程序
让我们先抛开设计模式,看看委托本身怎么用。
// 技术栈:C# / .NET 6+ 控制台应用程序
// 1. 声明一个委托类型。它规定:所有我能代表的方法,必须接受一个string参数,并且没有返回值。
public delegate void MessageHandler(string message);
class Program
{
// 2. 符合委托签名的方法A:控制台打印
static void DisplayOnConsole(string msg)
{
Console.WriteLine($"[控制台] 收到消息:{msg}");
}
// 3. 符合委托签名的方法B:记录到文件(这里模拟为写入另一行)
static void LogToFile(string msg)
{
Console.WriteLine($"[文件日志] 记录消息:{msg} - {DateTime.Now}");
}
static void Main()
{
// 4. 创建委托实例,并“记住”第一个方法
MessageHandler myHandler = DisplayOnConsole;
// 5. 通过委托调用方法,这就像“拨打电话”
myHandler("你好,世界!");
// 输出:[控制台] 收到消息:你好,世界!
// 6. 委托可以组合(多播委托),使用 += 添加“电话号码”
myHandler += LogToFile; // 现在myHandler里存了两个方法引用
Console.WriteLine("\n--- 组合委托调用 ---");
myHandler("天气晴转多云。");
// 输出:
// [控制台] 收到消息:天气晴转多云。
// [文件日志] 记录消息:天气晴转多云。 - [当前时间]
// 7. 也可以移除,使用 -=
myHandler -= DisplayOnConsole;
Console.WriteLine("\n--- 移除控制台显示后 ---");
myHandler("只剩文件日志了。");
// 输出:[文件日志] 记录消息:只剩文件日志了。 - [当前时间]
}
}
通过这个例子,我们看到委托MessageHandler就像一个呼叫列表。myHandler += ...就是订阅(留下联系方式),myHandler -= ...就是取消订阅,myHandler(“消息”)就是发布通知(拨打电话)。观察者模式的雏形已经出现了。
三、事件:为委托穿上“安全防护服”
直接用委托虽然灵活,但有一个问题:订阅者可以“为所欲为”。比如,订阅者可以直接用=赋值,覆盖掉之前所有的订阅者;或者,订阅者可以主动“拨打电话”(myHandler.Invoke(...)),这违背了“只有发布者才能通知”的原则。
事件(Event)就是为了解决这些问题而生的。事件是封装了的委托,它只允许外部进行+=(订阅)和-=(取消订阅)操作,而触发(调用)的权限则牢牢控制在定义它的类(发布者)内部。
让我们用事件来改造一个简单的“气象站”例子。
// 技术栈:C# / .NET 6+ 控制台应用程序
// 气象数据类,作为消息传递的载体
public class WeatherData
{
public float Temperature { get; set; }
public float Humidity { get; set; }
public float Pressure { get; set; }
}
// 发布者:气象站
public class WeatherStation
{
// 1. 声明一个事件。使用内置的 EventHandler<T> 委托类型,这是标准做法。
// EventHandler<T> 要求事件处理方法签名是:void (object sender, TEventArgs args)
public event EventHandler<WeatherData>? WeatherChanged;
private WeatherData _currentData = new WeatherData { Temperature = 26.0f, Humidity = 65.0f, Pressure = 1013.0f };
// 模拟气象数据更新
public void SetMeasurements(float temp, float humidity, float pressure)
{
_currentData.Temperature = temp;
_currentData.Humidity = humidity;
_currentData.Pressure = pressure;
// 2. 数据更新后,通知所有订阅者
// 这里先检查事件是否有订阅者(不为null),是良好的编程习惯
OnWeatherChanged(_currentData);
}
// 3. 定义触发事件的方法。通常以 On 开头,受保护虚方法,便于派生类控制触发逻辑。
protected virtual void OnWeatherChanged(WeatherData data)
{
// 这种赋值给局部变量的方式是线程安全的
WeatherChanged?.Invoke(this, data);
}
}
// 订阅者A:当前状态显示屏
public class CurrentConditionsDisplay
{
// 订阅事件的方法
public void Subscribe(WeatherStation station)
{
station.WeatherChanged += HandleWeatherChanged;
}
// 取消订阅
public void Unsubscribe(WeatherStation station)
{
station.WeatherChanged -= HandleWeatherChanged;
}
// 事件处理方法:必须符合 EventHandler<WeatherData> 的签名
private void HandleWeatherChanged(object? sender, WeatherData data)
{
// sender 是发布者对象,这里就是 WeatherStation 实例
Console.WriteLine($"【当前状态】温度:{data.Temperature:F1}°C, 湿度:{data.Humidity:F1}%, 气压:{data.Pressure:F1}hPa");
}
}
// 订阅者B:气象统计显示器
public class StatisticsDisplay
{
private float _maxTemp = float.MinValue;
private float _minTemp = float.MaxValue;
public void Subscribe(WeatherStation station)
{
station.WeatherChanged += HandleWeatherChanged;
}
private void HandleWeatherChanged(object? sender, WeatherData data)
{
_maxTemp = Math.Max(_maxTemp, data.Temperature);
_minTemp = Math.Min(_minTemp, data.Temperature);
Console.WriteLine($"【气象统计】历史最高温:{_maxTemp:F1}°C, 历史最低温:{_minTemp:F1}°C");
}
}
class Program
{
static void Main()
{
// 创建发布者
WeatherStation station = new WeatherStation();
// 创建订阅者
CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay();
StatisticsDisplay statsDisplay = new StatisticsDisplay();
// 订阅
currentDisplay.Subscribe(station);
statsDisplay.Subscribe(station);
Console.WriteLine("第一次测量数据更新:");
station.SetMeasurements(28.5f, 70.0f, 1012.0f);
Console.WriteLine();
Console.WriteLine("第二次测量数据更新:");
station.SetMeasurements(22.0f, 90.0f, 1015.0f);
Console.WriteLine();
// 当前状态显示屏取消订阅
currentDisplay.Unsubscribe(station);
Console.WriteLine("【当前状态显示屏已取消订阅】");
Console.WriteLine("第三次测量数据更新:");
station.SetMeasurements(25.0f, 80.0f, 1014.0f);
}
}
运行这个程序,你会看到不同的显示屏(订阅者)独立地接收并处理气象站(发布者)的数据更新,并且可以自由地订阅和取消订阅。事件关键字event确保了WeatherChanged只能在WeatherStation类内部触发(Invoke),外部只能进行+=和-=操作,这就是那层“安全防护服”。
四、深入标准事件模式与关联技术
在上面的例子中,我们使用了EventHandler<TEventArgs>这个.NET框架内置的泛型委托。这是标准事件模式的体现。其中TEventArgs必须派生自EventArgs基类。对于不传递额外数据的事件,可以直接使用非泛型的EventHandler委托。
有时候,你可能需要自定义事件参数,以传递更丰富的信息:
// 技术栈:C# / .NET 6+ 控制台应用程序
// 自定义事件参数类,继承自 EventArgs
public class TemperatureChangedEventArgs : EventArgs
{
public float OldTemperature { get; }
public float NewTemperature { get; }
public DateTime ChangeTime { get; }
public TemperatureChangedEventArgs(float oldTemp, float newTemp)
{
OldTemperature = oldTemp;
NewTemperature = newTemp;
ChangeTime = DateTime.Now;
}
}
public class AdvancedWeatherStation
{
private float _temperature;
// 使用自定义事件参数声明事件
public event EventHandler<TemperatureChangedEventArgs>? TemperatureChanged;
public float Temperature
{
get => _temperature;
set
{
if (Math.Abs(_temperature - value) > 0.01) // 温度有显著变化
{
float oldTemp = _temperature;
_temperature = value;
// 触发事件,并传递详细的变化信息
OnTemperatureChanged(oldTemp, _temperature);
}
}
}
protected virtual void OnTemperatureChanged(float oldTemp, float newTemp)
{
TemperatureChanged?.Invoke(this, new TemperatureChangedEventArgs(oldTemp, newTemp));
}
}
关联技术:Lambda表达式与匿名方法
在订阅事件时,除了像之前那样定义一个具名方法(如HandleWeatherChanged),我们还可以使用更简洁的Lambda表达式,特别适用于简单或一次性的处理逻辑。
// 技术栈:C# / .NET 6+ 控制台应用程序
WeatherStation station = new WeatherStation();
// 使用Lambda表达式直接订阅
station.WeatherChanged += (sender, data) =>
{
Console.WriteLine($"[Lambda订阅] 嘿!温度刚刚变成了 {data.Temperature}°C");
};
// 使用匿名方法(C# 2.0风格,现在较少用)
station.WeatherChanged += delegate (object sender, WeatherData data)
{
Console.WriteLine($"[匿名方法] 湿度是 {data.Humidity}%");
};
这种方式非常方便,但要注意:如果你需要取消订阅,使用Lambda表达式或匿名方法时,你必须将委托引用保存到一个变量中,因为-=操作需要与之前+=的委托实例完全一致。
EventHandler<WeatherData> handler = (sender, data) => Console.WriteLine("临时监听");
station.WeatherChanged += handler;
// ... 一些操作后
station.WeatherChanged -= handler; // 正确取消订阅
五、应用场景、优缺点与注意事项
应用场景:
- UI编程(WinForms, WPF, ASP.NET Core等): 这是事件最典型的应用。按钮点击(
Click)、文本框内容改变(TextChanged)、页面加载(Load)都是事件。 - 异步或后台任务通知: 例如,一个后台下载任务,可以通过
ProgressChanged事件报告进度,通过DownloadCompleted事件通知完成。 - 业务逻辑解耦: 如订单系统。订单创建(
OrderCreated)事件触发后,库存系统、物流系统、积分系统等监听者可以各自执行相应的逻辑,而订单模块无需关心它们的具体实现。 - 中间件或插件架构: 框架定义一系列事件(如
ApplicationStart,RequestBegin),插件通过订阅这些事件来注入自己的功能。
技术优点:
- 松耦合: 发布者完全不知道也不依赖具体的订阅者,只知道一个约定的委托签名。系统易于扩展和维护。
- 灵活性高: 可以动态地添加或移除订阅者,运行时配置性强。
- 符合.NET框架规范: 标准事件模式被整个.NET生态广泛采用,集成度高。
潜在缺点与注意事项:
- 内存泄漏风险: 这是最常见的问题。如果订阅者(例如一个UI窗体)订阅了长生命周期发布者(例如一个全局单例)的事件,并且忘记取消订阅,那么发布者持有的委托引用会阻止垃圾回收器回收订阅者对象,即使订阅者已不再使用。
- 解决方案: 在订阅者生命周期结束时(如窗体的
FormClosed事件中)主动取消订阅。
- 解决方案: 在订阅者生命周期结束时(如窗体的
- 事件顺序不确定: 当多个订阅者订阅同一事件时,它们的执行顺序通常是不确定的(与订阅顺序有关,但不应依赖此顺序)。如果业务逻辑依赖执行顺序,需要更复杂的机制。
- 异常处理: 在事件触发(
Invoke)过程中,如果某个订阅者的处理方法抛出异常,会中断后续订阅者的调用。发布者通常无法妥善处理所有订阅者可能抛出的异常。- 解决方案: 在发布者的
OnXXX方法中,可以遍历委托调用列表,对每个调用进行try-catch。
- 解决方案: 在发布者的
- 性能考虑: 对于性能极其苛刻的场景,频繁的事件触发和大量的订阅者可能会带来开销。但在绝大多数应用中,这点开销可忽略不计。
六、总结
C#中的委托和事件,是观察者模式在.NET平台上的自然表达和强力支撑。委托提供了“方法引用”的能力,是构建回调机制和异步模式的基石;而事件则在委托之上添加了封装和保护,确保了“发布-订阅”模型的安全性和规范性。
理解它们的关键在于转变思维:从“A直接调用B”的命令式思维,转变为“A发生了某事,至于谁关心、要做什么,A不负责”的事件驱动思维。这种思维模式对于构建松耦合、可扩展、响应式的现代软件系统至关重要。
通过本文从生活场景类比,到委托基础,再到事件封装和标准模式演进的讲解,并结合完整的代码示例,希望你能深刻体会到,委托与事件不仅仅是C#的语法特性,更是构建灵活软件架构的核心设计工具。记住最佳实践,警惕注意事项,你就能在项目中游刃有余地运用这一强大模式。
评论