掌握 C# 事件:异步编程与委托
导言
在现代软件开发中,构建响应迅速、可扩展且松耦合的应用程序至关重要。C# 中的委托(Delegates)、事件(Events)和异步编程(Asynchronous Programming)是实现这些目标的核心机制。它们共同构成了 C# 平台处理回调、通知和并发操作的强大基石。
委托作为类型安全的函数指针,为方法提供了灵活的引用和调用方式。事件则是在委托的基础上,提供了一种标准的发布-订阅(Publish-Subscribe)模式,使得对象能够在特定情况发生时通知其他对象,而无需紧密耦合。然而,当事件处理程序执行耗时操作时,可能会阻塞主线程,影响应用程序的响应性。这时,将异步编程(尤其是 async/await)与事件结合使用,就能优雅地解决这一问题,确保用户界面的流畅或后端服务的效率。
本文将深入探讨 C# 委托和事件的原理与用法,从基础概念到高级应用。我们将详细解释如何声明、使用委托,以及如何定义、触发和订阅事件。更重要的是,我们将重点介绍如何将异步编程范式融入事件处理机制,以构建高性能、非阻塞的应用程序。通过本文的学习,您将能够掌握 C# 事件的精髓,并有效利用异步编程来提升应用的响应性和用户体验。
委托 (Delegates) 的基础
在 C# 中,委托是一种类型,它安全地封装了一个或多个方法。你可以将委托理解为一种“函数指针”,但它比 C++ 中的函数指针更安全,更面向对象。委托的本质是定义了一个方法的签名(包括返回类型和参数列表),任何符合这个签名的方法都可以赋值给这个委托实例。
什么是委托?
委托主要用于:
1. 回调机制:当某个操作完成或发生特定事件时,通知其他对象执行预定义的方法。
2. 事件处理:C# 事件的底层实现就是基于委托。
3. 多态性:实现方法层面的多态,使代码更加灵活和可扩展。
4. 异步编程:在早期的异步编程模式(如 APM 模式)中扮演重要角色,尽管现在 async/await 更为常见,但委托仍是其基础。
声明和使用委托
声明委托使用 delegate 关键字,后跟方法的返回类型、委托名称和参数列表。
语法
csharp
public delegate void MyDelegate(string message);
public delegate int CalculatorDelegate(int a, int b);
上面声明了两个委托:
– MyDelegate 可以引用任何接受一个 string 参数且没有返回值的(void)方法。
– CalculatorDelegate 可以引用任何接受两个 int 参数且返回一个 int 值的方法。
实例化委托
委托可以引用静态方法或实例方法。
“`csharp
using System;
public class DelegateExample
{
// 1. 声明委托
public delegate void PrintMessageDelegate(string message);
// 静态方法
public static void DisplayMessage(string msg)
{
Console.WriteLine($"静态方法显示: {msg}");
}
// 实例方法
public void LogMessage(string msg)
{
Console.WriteLine($"实例方法记录: {msg}");
}
public static void Main(string[] args)
{
// 2. 实例化委托并引用静态方法
PrintMessageDelegate printDelegate1 = DisplayMessage;
printDelegate1("Hello from static method!");
// 实例化委托并引用实例方法
DelegateExample example = new DelegateExample();
PrintMessageDelegate printDelegate2 = example.LogMessage;
printDelegate2("Hello from instance method!");
// 也可以使用匿名方法或 Lambda 表达式
PrintMessageDelegate printDelegate3 = delegate(string msg)
{
Console.WriteLine($"匿名方法显示: {msg}");
};
printDelegate3("Hello from anonymous method!");
PrintMessageDelegate printDelegate4 = msg => Console.WriteLine($"Lambda 表达式显示: {msg}");
printDelegate4("Hello from lambda expression!");
}
}
“`
多播委托 (Multicast Delegates)
委托的一个强大特性是它可以引用多个方法,这被称为“多播委托”。当调用多播委托时,它所引用的所有方法都会按照添加的顺序依次执行。
“`csharp
using System;
public class MulticastDelegateExample
{
public delegate void NotificationDelegate(string message);
public static void SendEmail(string msg)
{
Console.WriteLine($"发送邮件: {msg}");
}
public static void SendSMS(string msg)
{
Console.WriteLine($"发送短信: {msg}");
}
public static void Main(string[] args)
{
NotificationDelegate notifyAll = null;
// 添加方法到委托链
notifyAll += SendEmail;
notifyAll += SendSMS;
// 调用委托,所有引用的方法都会被执行
Console.WriteLine("--- 第一次通知 ---");
notifyAll("会议即将开始!");
// 移除一个方法
notifyAll -= SendEmail;
Console.WriteLine("\n--- 第二次通知 (移除邮件通知) ---");
notifyAll("请准时参加会议!");
// 如果所有方法都被移除,调用委托将不会有任何操作,但最好进行 null 检查
notifyAll -= SendSMS;
if (notifyAll != null)
{
notifyAll("这条消息不会被发送。");
}
else
{
Console.WriteLine("\n所有订阅者都已移除。");
}
}
}
“`
需要注意的是,如果多播委托引用的方法有返回值,那么调用委托时只会返回链中最后一个方法的返回值。如果链中的某个方法抛出异常,整个链的执行会中断。
委托的实际应用场景
- 通用回调:例如,将一个委托作为参数传递给一个方法,让该方法在完成其工作后调用这个委托。
- LINQ 查询:
Where(),Select(),OrderBy()等方法都大量使用了Func和Action委托。 - 线程池:
ThreadPool.QueueUserWorkItem方法使用WaitCallback委托来执行异步操作。
匿名方法和 Lambda 表达式与委托
C# 提供了匿名方法 (C# 2.0) 和 Lambda 表达式 (C# 3.0) 语法糖,极大地简化了委托的使用。它们允许你以内联方式定义方法,而无需显式创建单独的方法。
-
匿名方法:
csharp
PrintMessageDelegate printDelegate = delegate(string msg)
{
Console.WriteLine($"匿名方法: {msg}");
}; -
Lambda 表达式: 更加简洁,是匿名方法的进一步演进。
“`csharp
// 无参数,无返回值
Action greet = () => Console.WriteLine(“Hello!”);// 有参数,无返回值
Actionprint = message => Console.WriteLine(message); // 有参数,有返回值
Funcadd = (a, b) => a + b;
``Action和Func` 是 .NET 框架预定义的泛型委托,分别用于表示无返回值和有返回值的方法,它们极大地减少了自定义委托的需要。 -
Action:public delegate void Action(); Action<T>:public delegate void Action<T>(T obj);Func<TResult>:public delegate TResult Func<TResult>();Func<T, TResult>:public delegate TResult Func<T, TResult>(T arg);
掌握委托是理解 C# 事件和许多其他高级编程模式(如 LINQ、异步编程)的关键。它提供了一种强大而灵活的方式来处理代码中的行为引用。
事件 (Events) 的核心概念
事件是 C# 中实现发布-订阅(Publish-Subscribe)设计模式的一种特殊类型的多播委托。它提供了一种机制,允许一个对象(发布者)在发生某些特定动作时通知其他对象(订阅者),而无需这些对象之间有直接的依赖关系。这种松耦合的通信方式是构建可扩展和可维护应用程序的关键。
什么是事件?
从技术角度看,事件是委托类型的一个特殊实例,并且具有受限的访问权限。event 关键字修饰的委托成员只能在声明它的类内部被调用(触发),而外部类只能订阅或取消订阅它。这确保了事件的封装性和安全性,即只有发布者能够决定何时触发事件。
声明和发布事件
C# 事件通常遵循 .NET 的标准模式:
– 事件源(发布者):定义事件的类。
– 事件处理程序(订阅者):响应事件的方法。
– 事件参数(EventArgs):包含事件相关数据的类。
event 关键字
使用 event 关键字将一个委托声明为一个事件。
“`csharp
using System;
// 1. 定义自定义事件参数类(可选,但推荐)
// 继承自 EventArgs,并包含事件相关数据
public class TemperatureChangedEventArgs : EventArgs
{
public int OldTemperature { get; }
public int NewTemperature { get; }
public TemperatureChangedEventArgs(int oldTemp, int newTemp)
{
OldTemperature = oldTemp;
NewTemperature = newTemp;
}
}
// 2. 定义事件发布者
public class Thermostat
{
private int currentTemperature;
public int CurrentTemperature
{
get { return currentTemperature; }
set
{
if (currentTemperature != value)
{
int oldTemp = currentTemperature;
currentTemperature = value;
// 3. 触发事件
OnTemperatureChanged(oldTemp, currentTemperature);
}
}
}
// 声明事件
// 推荐使用 EventHandler<TEventArgs> 泛型委托
public event EventHandler<TemperatureChangedEventArgs> TemperatureChanged;
// 触发事件的保护方法
// 遵循 .NET 约定,方法名为 OnEventName
protected virtual void OnTemperatureChanged(int oldTemp, int newTemp)
{
// 确保有订阅者,避免 NullReferenceException
// 线程安全的方式 (C# 6.0+)
TemperatureChanged?.Invoke(this, new TemperatureChangedEventArgs(oldTemp, newTemp));
// 老式写法,但线程不安全,可能在检查后到调用前被置为 null
// if (TemperatureChanged != null)
// {
// TemperatureChanged(this, new TemperatureChangedEventArgs(oldTemp, newTemp));
// }
}
}
“`
EventHandler 和 EventHandler<TEventArgs>
.NET 框架提供了两种标准委托类型来简化事件的声明:
EventHandler:用于不带任何自定义数据的事件。其签名是void EventHandler(object sender, EventArgs e);EventHandler<TEventArgs>:用于带自定义数据的事件,其中TEventArgs是你的自定义事件参数类型,必须继承自EventArgs。其签名是void EventHandler<TEventArgs>(object sender, TEventArgs e);
约定:
– 事件处理程序的第一个参数是 object sender,它引用事件的发送者。
– 事件处理程序的第二个参数是 EventArgs e(或其派生类),它包含事件相关的数据。
订阅和取消订阅事件
其他类可以通过 += 运算符订阅事件,通过 -= 运算符取消订阅事件。
“`csharp
using System;
public class Heater
{
public void OnTemperatureChanged(object sender, TemperatureChangedEventArgs e)
{
if (e.NewTemperature < 20)
{
Console.WriteLine($”Heater: 温度过低 ({e.NewTemperature}°C),启动加热!”);
}
else
{
Console.WriteLine($”Heater: 温度适中 ({e.NewTemperature}°C)。”);
}
}
}
public class Cooler
{
public void OnTemperatureChanged(object sender, TemperatureChangedEventArgs e)
{
if (e.NewTemperature > 25)
{
Console.WriteLine($”Cooler: 温度过高 ({e.NewTemperature}°C),启动制冷!”);
}
else
{
Console.WriteLine($”Cooler: 温度适中 ({e.NewTemperature}°C)。”);
}
}
}
public class EventConsumer
{
public static void Main(string[] args)
{
Thermostat thermostat = new Thermostat();
Heater heater = new Heater();
Cooler cooler = new Cooler();
// 订阅事件
thermostat.TemperatureChanged += heater.OnTemperatureChanged;
thermostat.TemperatureChanged += cooler.OnTemperatureChanged;
thermostat.TemperatureChanged += (sender, e) =>
{
Console.WriteLine($"Anonymous Consumer: 温度从 {e.OldTemperature}°C 变为 {e.NewTemperature}°C。");
};
Console.WriteLine("设置温度到 18°C:");
thermostat.CurrentTemperature = 18; // 触发事件
Console.WriteLine("\n设置温度到 22°C:");
thermostat.CurrentTemperature = 22; // 触发事件
Console.WriteLine("\n设置温度到 28°C:");
thermostat.CurrentTemperature = 28; // 触发事件
// 取消订阅事件
Console.WriteLine("\nCooler 停止工作,取消订阅...");
thermostat.TemperatureChanged -= cooler.OnTemperatureChanged;
Console.WriteLine("\n再次设置温度到 28°C (Cooler 不再响应):");
thermostat.CurrentTemperature = 28; // 再次触发事件
thermostat.CurrentTemperature = 30; // 再次触发事件
Console.WriteLine("\n设置温度到 15°C:");
thermostat.CurrentTemperature = 15; // 触发事件
}
}
“`
事件的访问修饰符和最佳实践
- 访问修饰符:事件可以有
public,protected,internal,private等访问修饰符,这决定了哪些类可以订阅或取消订阅它。通常,事件被声明为public,以便外部类可以订阅。 - 保护触发方法:将触发事件的方法(例如
OnTemperatureChanged)声明为protected virtual是一个常见的模式。protected允许派生类触发事件,virtual允许派生类重写该行为。 - Null 检查:在触发事件之前,务必进行 null 检查(使用
?.Invoke或if (EventName != null)),以防止在没有订阅者时发生NullReferenceException。 - 线程安全:
?.Invoke运算符是线程安全的,因为它在调用前将事件的当前值复制到一个临时变量中。 - 避免将事件声明为
virtual:虽然触发事件的方法可以是virtual,但事件本身通常不声明为virtual。这是因为事件的订阅者列表是委托链,直接重写virtual event会导致基类的订阅者丢失。通过protected virtual OnEventName方法来提供派生类的自定义行为是更好的选择。
掌握事件是理解 C# 异步编程、UI 框架(如 WinForms, WPF)以及其他许多基于通知的模式的基础。它鼓励松耦合设计,使系统更易于维护和扩展。
异步编程与事件
在传统的同步事件处理中,事件处理程序会阻塞事件源的线程,直到其自身完成执行。这在 GUI 应用程序中尤其成问题,因为它可能导致 UI 冻结,响应迟钝;在服务器端应用程序中,也可能导致处理请求的线程被长时间占用,影响系统吞吐量。为了解决这些问题,我们需要将异步编程与事件结合起来。
为什么事件需要异步处理?
- 避免阻塞 UI 线程或主线程:在 UI 应用程序中,如果事件处理程序执行 I/O 操作(如网络请求、文件读写)或计算密集型任务,UI 将变得无响应。异步处理可以将这些操作卸载到后台线程,保持 UI 线程的流畅。
- 提高响应性:应用程序能够更快地响应用户输入或其他事件,因为耗时操作不再阻塞主执行流。
- 资源效率:对于服务器端应用程序,异步操作允许线程在等待 I/O 完成时被释放,去处理其他请求,从而提高服务器的并发处理能力。
使用 async/await 处理事件
C# 的 async 和 await 关键字极大地简化了异步编程。你可以在事件处理程序中直接使用它们。
“`csharp
using System;
using System.Threading.Tasks;
// 假设我们有一个事件发布者,它触发一个表示任务开始的事件
public class TaskMonitor
{
public event EventHandler
public void StartLongRunningTask(string taskName)
{
Console.WriteLine($"[Monitor] 任务 '{taskName}' 启动...");
TaskStarted?.Invoke(this, taskName); // 触发事件
// 实际的耗时任务可能在此之后或由事件处理程序自行异步执行
}
}
public class AsyncEventHandlerExample
{
public static async void LongRunningEventHandler(object sender, string taskName)
{
Console.WriteLine($”[Handler 1] 接收到任务开始通知:'{taskName}’。开始异步处理…”);
await Task.Delay(3000); // 模拟一个 3 秒的异步操作
Console.WriteLine($”[Handler 1] 异步处理任务 ‘{taskName}’ 完成。”);
}
public static async void AnotherAsyncEventHandler(object sender, string taskName)
{
Console.WriteLine($"[Handler 2] 接收到任务开始通知:'{taskName}'。开始另一个异步处理...");
await Task.Delay(2000); // 模拟一个 2 秒的异步操作
Console.WriteLine($"[Handler 2] 另一个异步处理任务 '{taskName}' 完成。");
}
public static void SyncEventHandler(object sender, string taskName)
{
Console.WriteLine($"[Handler 3] 接收到任务开始通知:'{taskName}'。同步处理中...");
// 模拟一个同步耗时操作
System.Threading.Thread.Sleep(1000);
Console.WriteLine($"[Handler 3] 同步处理任务 '{taskName}' 完成。");
}
public static void Main(string[] args)
{
TaskMonitor monitor = new TaskMonitor();
// 订阅异步事件处理程序
monitor.TaskStarted += LongRunningEventHandler;
monitor.TaskStarted += AnotherAsyncEventHandler;
// 订阅同步事件处理程序
monitor.TaskStarted += SyncEventHandler;
monitor.StartLongRunningTask("文件下载");
Console.WriteLine("\n主线程继续执行,等待所有事件处理完成...");
// 由于事件处理程序是 async void,主线程无法直接等待它们完成。
// 这需要特殊的处理,见下文“异步事件模式”。
System.Threading.Thread.Sleep(5000); // 留出时间让异步处理完成
Console.WriteLine("\n主线程结束。");
}
}
“`
在上面的例子中,LongRunningEventHandler 和 AnotherAsyncEventHandler 被标记为 async void。这意味着它们可以执行异步操作,但事件发布者无法 await 它们完成。async void 事件处理程序是“即发即忘”的,它们在执行到第一个 await 时立即返回给调用者,但如果它们内部抛出未处理的异常,则会直接崩溃进程。因此,async void 应该谨慎使用,主要限于顶级事件处理程序。
注意异常处理
在 async void 方法中,任何未捕获的异常都会在同步执行上下文中重新抛出,这意味着它将终止进程,而不是被 await 调用者捕获。因此,在 async void 事件处理程序内部,务必包含完善的异常处理(如 try-catch 块)。
csharp
public static async void FaultyEventHandler(object sender, string taskName)
{
try
{
Console.WriteLine($"[Faulty Handler] 开始处理 '{taskName}'...");
await Task.Delay(1000);
throw new InvalidOperationException("Something went wrong in async event handler!");
}
catch (Exception ex)
{
Console.WriteLine($"[Faulty Handler] 捕获到异常: {ex.Message}");
}
Console.WriteLine($"[Faulty Handler] 处理任务 '{taskName}' 结束。");
}
异步事件模式
当事件发布者需要知道所有订阅者何时完成其异步操作,或者需要聚合它们的异步结果时,async void 就不适用了。这时,我们需要一种更高级的异步事件模式。一种常见的方法是让事件本身返回 Task。
-
定义异步事件委托:
我们可以定义一个返回Task的委托,或者使用Func<TEventArgs, Task>。“`csharp
public class CustomEventArgs : EventArgs { / … / }
public delegate Task AsyncEventHandler(object sender, TEventArgs e); public class AsyncEventSource
{
public event AsyncEventHandlerMyAsyncEvent; public async Task RaiseMyAsyncEvent() { CustomEventArgs args = new CustomEventArgs(); // 获取所有订阅者 var handlers = MyAsyncEvent?.GetInvocationList() .Cast<AsyncEventHandler<CustomEventArgs>>(); if (handlers != null) { // 创建一个 Task 列表,等待所有异步处理程序完成 List<Task> handlerTasks = new List<Task>(); foreach (var handler in handlers) { handlerTasks.Add(handler(this, args)); } await Task.WhenAll(handlerTasks); // 等待所有任务完成 } }}
“` -
异步订阅者的实现:
订阅者可以实现async Task方法来处理事件。csharp
public class AsyncEventSubscriber
{
public async Task HandleMyAsyncEvent(object sender, CustomEventArgs e)
{
Console.WriteLine($"[Subscriber] 开始异步处理事件...");
await Task.Delay(2000); // 模拟耗时操作
Console.WriteLine($"[Subscriber] 异步处理事件完成。");
}
} -
如何正确等待所有异步事件处理完成:
在发布者中,通过GetInvocationList()获取所有订阅的方法,然后逐一调用它们并收集返回的Task对象。最后,使用Task.WhenAll()等待所有这些Task完成。这样,事件发布者就能确保所有订阅者的异步操作都已完成。“`csharp
public static async Task Main(string[] args)
{
AsyncEventSource source = new AsyncEventSource();
AsyncEventSubscriber subscriber1 = new AsyncEventSubscriber();
AsyncEventSubscriber subscriber2 = new AsyncEventSubscriber();source.MyAsyncEvent += subscriber1.HandleMyAsyncEvent; source.MyAsyncEvent += subscriber2.HandleMyAsyncEvent; Console.WriteLine("主线程: 触发异步事件..."); await source.RaiseMyAsyncEvent(); // 等待所有订阅者完成异步操作 Console.WriteLine("主线程: 所有异步事件处理程序已完成。");}
}
“`
这种模式允许事件发布者拥有对异步处理程序完成时机的控制,但它也增加了复杂性,因为事件发布者现在需要管理这些 Task。在许多情况下,简单的 async void 已经足够,特别是当事件处理程序不需要向发布者报告完成状态时。
选择 async void 还是 async Task 事件处理取决于你的具体需求:
– 如果事件处理程序是顶级的,不需要被等待,并且异常可以在内部处理,async void 可以是一个简单的选择。
– 如果需要等待所有事件处理程序完成,或者需要聚合它们的异步结果,那么定义一个返回 Task 的事件委托,并使用 Task.WhenAll 来管理它们是更健壮的方法。
综合示例:文件处理器的异步通知
我们将构建一个文件处理器,它能够读取一个文件,并在处理过程中触发进度事件和完成事件。一些事件处理程序将是同步的(如日志记录),另一些将是异步的(如将处理结果上传到云服务或发送通知)。
场景描述
FileProcessor类:负责模拟文件处理,并在处理过程中触发事件。FileProcessingEventArgs:自定义事件参数,包含处理的文件名和进度百分比。FileProcessedEventArgs:自定义事件参数,包含处理完成的文件名和处理结果(成功/失败)。ProgressReported事件:报告文件处理进度。ProcessingCompleted事件:报告文件处理完成。
Logger类:订阅ProgressReported和ProcessingCompleted事件,同步记录日志。CloudUploader类:订阅ProcessingCompleted事件,异步模拟将文件上传到云服务。Notifier类:订阅ProcessingCompleted事件,异步模拟发送通知(如邮件)。
示例代码
1. 自定义事件参数
“`csharp
using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;
// 文件处理进度事件参数
public class FileProcessingEventArgs : EventArgs
{
public string FileName { get; }
public int Percentage { get; }
public FileProcessingEventArgs(string fileName, int percentage)
{
FileName = fileName;
Percentage = percentage;
}
}
// 文件处理完成事件参数
public class FileProcessedEventArgs : EventArgs
{
public string FileName { get; }
public bool Success { get; }
public string Message { get; }
public FileProcessedEventArgs(string fileName, bool success, string message)
{
FileName = fileName;
Success = success;
Message = message;
}
}
“`
2. 文件处理器 (事件发布者)
“`csharp
public class FileProcessor
{
// 声明进度报告事件
public event EventHandler
// 声明处理完成事件
// 为了演示异步等待,我们使用 Func<Task> 作为事件处理程序的签名
// 但更常见和标准的做法仍然是 EventHandler<TEventArgs>,由订阅者内部处理 async/await
// 这里我们将使用标准的 EventHandler<TEventArgs>,并演示如何在主程序中等待 async void 处理程序的完成
public event EventHandler<FileProcessedEventArgs> ProcessingCompleted;
// 触发进度事件的保护方法
protected virtual void OnProgressReported(string fileName, int percentage)
{
ProgressReported?.Invoke(this, new FileProcessingEventArgs(fileName, percentage));
}
// 触发完成事件的保护方法
protected virtual void OnProcessingCompleted(string fileName, bool success, string message)
{
ProcessingCompleted?.Invoke(this, new FileProcessedEventArgs(fileName, success, message));
}
// 模拟文件处理的异步方法
public async Task ProcessFileAsync(string fileName)
{
Console.WriteLine($"\n[Processor] 开始处理文件: {fileName}");
try
{
for (int i = 0; i <= 100; i += 10)
{
await Task.Delay(200); // 模拟耗时操作
OnProgressReported(fileName, i);
}
OnProcessingCompleted(fileName, true, "文件处理成功!");
}
catch (Exception ex)
{
OnProcessingCompleted(fileName, false, $"文件处理失败: {ex.Message}");
}
Console.WriteLine($"[Processor] 文件处理完成: {fileName}");
}
}
“`
3. 事件订阅者
“`csharp
public class Logger
{
public void OnProgressReported(object sender, FileProcessingEventArgs e)
{
Console.WriteLine($”[Logger] 文件 ‘{e.FileName}’ 进度: {e.Percentage}%”);
}
public void OnProcessingCompleted(object sender, FileProcessedEventArgs e)
{
Console.WriteLine($"[Logger] 文件 '{e.FileName}' {(e.Success ? "处理成功" : "处理失败")}. 消息: {e.Message}");
}
}
public class CloudUploader
{
public async void OnProcessingCompleted(object sender, FileProcessedEventArgs e)
{
if (e.Success)
{
Console.WriteLine($”[CloudUploader] 开始异步上传文件 ‘{e.FileName}’ 到云端…”);
await Task.Delay(3000); // 模拟上传耗时
Console.WriteLine($”[CloudUploader] 文件 ‘{e.FileName}’ 已成功上传到云端。”);
}
else
{
Console.WriteLine($”[CloudUploader] 文件 ‘{e.FileName}’ 处理失败,不执行上传。”);
}
}
}
public class Notifier
{
public async void OnProcessingCompleted(object sender, FileProcessedEventArgs e)
{
Console.WriteLine($”[Notifier] 异步准备发送通知给管理员关于文件 ‘{e.FileName}’…”);
await Task.Delay(2000); // 模拟发送通知耗时
Console.WriteLine($”[Notifier] 通知已发送,文件 ‘{e.FileName}’ {(e.Success ? “成功” : “失败”)}.”);
}
}
“`
4. 主程序 (订阅并运行)
“`csharp
public class Program
{
public static async Task Main(string[] args)
{
FileProcessor processor = new FileProcessor();
Logger logger = new Logger();
CloudUploader uploader = new CloudUploader();
Notifier notifier = new Notifier();
// 订阅事件
processor.ProgressReported += logger.OnProgressReported;
processor.ProcessingCompleted += logger.OnProcessingCompleted;
processor.ProcessingCompleted += uploader.OnProcessingCompleted;
processor.ProcessingCompleted += notifier.OnProcessingCompleted;
Console.WriteLine("--- 场景一: 处理文件 'document.pdf' ---");
await processor.ProcessFileAsync("document.pdf");
// 由于 CloudUploader 和 Notifier 使用的是 async void 事件处理程序,
// ProcessFileAsync 方法在它们完全执行完毕之前就已经返回了。
// 为了确保主程序能够看到它们的输出,我们需要额外等待一段时间。
// 在实际应用中,如果需要等待这些异步处理程序完成,就应该使用“异步事件模式”
// 让事件处理程序返回 Task,并在 OnProcessingCompleted 中使用 Task.WhenAll。
Console.WriteLine("\n[Main] 文件处理器已完成,等待异步事件处理程序完成其工作...");
await Task.Delay(5000); // 等待所有 async void 异步事件处理程序完成
Console.WriteLine("\n--- 场景二: 处理文件 'image.jpg' ---");
// 模拟处理另一个文件
await processor.ProcessFileAsync("image.jpg");
Console.WriteLine("\n[Main] 文件处理器已完成,等待异步事件处理程序完成其工作...");
await Task.Delay(5000);
Console.WriteLine("\n[Main] 所有操作完成。");
}
}
``CloudUploader.OnProcessingCompleted
**重要提示**: 在上述示例中,和Notifier.OnProcessingCompleted方法被标记为async void。这意味着FileProcessor在触发ProcessingCompleted事件后,会立即继续执行而不等待这些异步处理程序完成。为了在Main方法中观察到它们的输出,我们不得不使用Task.Delay(5000)` 进行人工等待。
如果您需要确保所有异步事件处理程序都已完成才能继续,您应该采用之前“异步事件模式”中描述的方法:定义一个返回 Task 的事件委托,并在事件发布者的触发方法中,使用 GetInvocationList 收集所有 Task,然后用 Task.WhenAll 等待它们。这会使事件发布者和订阅者之间的耦合度略有增加,但在需要精确控制异步流程时是必要的。
结论
通过本文的深入探讨,我们全面审视了 C# 中委托、事件及其与异步编程的结合。我们了解到:
- 委托是 C# 类型安全的函数指针,它提供了引用和调用方法的强大机制,是事件和许多高级模式(如 LINQ)的基础。
Action和Func泛型委托极大简化了委托的使用。 - 事件是基于委托实现的发布-订阅模式,它使得对象能够以松耦合的方式相互通信。通过
event关键字,我们可以安全地声明、发布和订阅事件,并通过EventHandler<TEventArgs>约定传递事件数据。 - 异步编程与事件的结合是现代 C# 应用程序开发不可或缺的一部分。
async/await使得我们能够在事件处理程序中执行非阻塞的耗时操作,从而保持应用程序的响应性和流畅性。虽然async void简洁,但对于需要等待所有订阅者完成或聚合结果的场景,采用返回Task的异步事件模式更为健壮。
掌握这些核心概念对于构建高性能、可维护且响应迅速的 C# 应用程序至关重要。无论是桌面应用、Web 服务还是移动开发,合理利用委托、事件和异步编程,都将大大提升代码质量和用户体验。希望本文能够帮助您更深入地理解并熟练运用这些强大的 C# 特性。