掌握 C# 事件:异步编程与委托 – wiki基地

掌握 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() 等方法都大量使用了 FuncAction 委托。
  • 线程池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!”);

    // 有参数,无返回值
    Action print = message => Console.WriteLine(message);

    // 有参数,有返回值
    Func add = (a, b) => a + b;
    ``ActionFunc` 是 .NET 框架预定义的泛型委托,分别用于表示无返回值和有返回值的方法,它们极大地减少了自定义委托的需要。

  • Actionpublic 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));
    // }
}

}
“`

EventHandlerEventHandler<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 检查(使用 ?.Invokeif (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# 的 asyncawait 关键字极大地简化了异步编程。你可以在事件处理程序中直接使用它们。

“`csharp
using System;
using System.Threading.Tasks;

// 假设我们有一个事件发布者,它触发一个表示任务开始的事件
public class TaskMonitor
{
public event EventHandler TaskStarted;

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主线程结束。");
}

}
“`

在上面的例子中,LongRunningEventHandlerAnotherAsyncEventHandler 被标记为 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

  1. 定义异步事件委托
    我们可以定义一个返回 Task 的委托,或者使用 Func<TEventArgs, Task>

    “`csharp
    public class CustomEventArgs : EventArgs { // }
    public delegate Task AsyncEventHandler(object sender, TEventArgs e);

    public class AsyncEventSource
    {
    public event AsyncEventHandler MyAsyncEvent;

    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); // 等待所有任务完成
        }
    }
    

    }
    “`

  2. 异步订阅者的实现
    订阅者可以实现 async Task 方法来处理事件。

    csharp
    public class AsyncEventSubscriber
    {
    public async Task HandleMyAsyncEvent(object sender, CustomEventArgs e)
    {
    Console.WriteLine($"[Subscriber] 开始异步处理事件...");
    await Task.Delay(2000); // 模拟耗时操作
    Console.WriteLine($"[Subscriber] 异步处理事件完成。");
    }
    }

  3. 如何正确等待所有异步事件处理完成
    在发布者中,通过 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 类:订阅 ProgressReportedProcessingCompleted 事件,同步记录日志。
  • 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 ProgressReported;

// 声明处理完成事件
// 为了演示异步等待,我们使用 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.OnProcessingCompletedNotifier.OnProcessingCompleted方法被标记为async void。这意味着FileProcessor在触发ProcessingCompleted事件后,会立即继续执行而不等待这些异步处理程序完成。为了在Main方法中观察到它们的输出,我们不得不使用Task.Delay(5000)` 进行人工等待。

如果您需要确保所有异步事件处理程序都已完成才能继续,您应该采用之前“异步事件模式”中描述的方法:定义一个返回 Task 的事件委托,并在事件发布者的触发方法中,使用 GetInvocationList 收集所有 Task,然后用 Task.WhenAll 等待它们。这会使事件发布者和订阅者之间的耦合度略有增加,但在需要精确控制异步流程时是必要的。

结论

通过本文的深入探讨,我们全面审视了 C# 中委托、事件及其与异步编程的结合。我们了解到:

  • 委托是 C# 类型安全的函数指针,它提供了引用和调用方法的强大机制,是事件和许多高级模式(如 LINQ)的基础。ActionFunc 泛型委托极大简化了委托的使用。
  • 事件是基于委托实现的发布-订阅模式,它使得对象能够以松耦合的方式相互通信。通过 event 关键字,我们可以安全地声明、发布和订阅事件,并通过 EventHandler<TEventArgs> 约定传递事件数据。
  • 异步编程与事件的结合是现代 C# 应用程序开发不可或缺的一部分。async/await 使得我们能够在事件处理程序中执行非阻塞的耗时操作,从而保持应用程序的响应性和流畅性。虽然 async void 简洁,但对于需要等待所有订阅者完成或聚合结果的场景,采用返回 Task 的异步事件模式更为健壮。

掌握这些核心概念对于构建高性能、可维护且响应迅速的 C# 应用程序至关重要。无论是桌面应用、Web 服务还是移动开发,合理利用委托、事件和异步编程,都将大大提升代码质量和用户体验。希望本文能够帮助您更深入地理解并熟练运用这些强大的 C# 特性。

滚动至顶部