C# | 委托 | 事件 | 异步
委托(Delegate)和事件(Event)
在C#和C++中,委托(Delegate)与事件(Event)以及函数对象(Function Object)是实现回调机制或传递行为的重要工具。虽然它们在语法和具体实现上有所不同,但它们的核心思想是相似的:封装可调用的行为,以便可以在不同的上下文中执行。
1. C#中的委托(Delegate)
定义与作用
- 委托是一种类型安全的函数指针,用于封装方法的引用。
- 它允许将方法作为参数传递,或者存储方法以供后续调用。
- 委托可以指向静态方法或实例方法,并且支持多播(Multicast),即一个委托可以同时调用多个方法。
基本语法
// 定义一个委托类型
public delegate void MyDelegate(string message);
// 使用委托
public class Program
{
public static void Main()
{
// 创建委托实例并绑定方法
MyDelegate del = new MyDelegate(ShowMessage);
del("Hello, World!"); // 调用委托
}
public static void ShowMessage(string message)
{
Console.WriteLine(message);
}
}
特点
- 类型安全:委托在编译时检查方法签名是否匹配。
- 多播支持:通过
+=
操作符,可以将多个方法绑定到同一个委托。MyDelegate del = ShowMessage; del += AnotherMethod; del("Hello"); // 会依次调用ShowMessage和AnotherMethod
2. C#中的事件(Event)
定义与作用
- 事件是基于委托的一种特殊机制,通常用于实现发布-订阅模式。
- 它限制了外部对委托的直接调用,只能通过
+=
或-=
来添加或移除事件处理程序。
基本语法
// 定义事件
public class Publisher
{
// 声明事件
public event EventHandler<MyEventArgs> Notify;
public void DoSomething()
{
// 触发事件
Notify?.Invoke(this, new MyEventArgs("Event Triggered"));
}
}
// 自定义事件参数类
public class MyEventArgs : EventArgs
{
public string Message { get; }
public MyEventArgs(string message)
{
Message = message;
}
}
// 订阅事件
public class Subscriber
{
public void OnNotify(object sender, MyEventArgs e)
{
Console.WriteLine($"Received: {e.Message}");
}
}
// 使用示例
public class Program
{
public static void Main()
{
Publisher publisher = new Publisher();
Subscriber subscriber = new Subscriber();
// 订阅事件
publisher.Notify += subscriber.OnNotify;
// 触发事件
publisher.DoSomething();
}
}
特点
- 封装性:事件对外部隐藏了委托的具体实现,只能通过
+=
或-=
操作。 - 松耦合:发布者和订阅者之间没有直接依赖,适合构建模块化的系统。
3. C++中的函数对象(Function Object)
定义与作用
- 函数对象(也称为仿函数,Functor)是一个重载了
operator()
的类或结构体实例。 - 它可以像函数一样被调用,同时具有普通对象的特性(如保存状态)。
基本语法
#include <iostream>
using namespace std;
// 定义一个函数对象
class MyFunctor
{
public:
void operator()(string message) const
{
cout << "Message: " << message << endl;
}
};
int main()
{
// 创建函数对象实例
MyFunctor functor;
// 调用函数对象
functor("Hello, World!");
return 0;
}
特点
- 灵活性:函数对象可以保存状态,而普通函数不能。
class Counter { private: int count; public: Counter() : count(0) {} void operator()() { cout << "Count: " << ++count << endl; } }; int main() { Counter counter; counter(); // 输出 Count: 1 counter(); // 输出 Count: 2 return 0; }
- 性能优化:在某些情况下,函数对象比函数指针更高效,因为编译器可以对其进行内联优化。
4. 对比总结
特性 | C# 委托 | C# 事件 | C++ 函数对象 |
---|---|---|---|
本质 | 类型安全的函数指针 | 基于委托的发布-订阅机制 | 重载了operator() 的对象 |
用途 | 封装方法引用,支持多播 | 实现事件驱动的通信机制 | 封装行为,支持状态保存 |
类型安全 | 是 | 是 | 是 |
多播支持 | 支持 | 不直接支持 | 不支持 |
状态保存 | 不支持 | 不支持 | 支持 |
使用场景 | 回调、异步编程 | GUI事件、观察者模式 | STL算法、自定义行为封装 |
5. 示例对比:实现简单的回调机制
C# 委托实现
public delegate void Callback(string message);
public class Processor
{
public Callback OnComplete;
public void Process()
{
// 模拟处理逻辑
OnComplete?.Invoke("Processing Complete");
}
}
public class Program
{
public static void Main()
{
Processor processor = new Processor();
processor.OnComplete = message => Console.WriteLine(message);
processor.Process(); // 输出 Processing Complete
}
}
C++ 函数对象实现
#include <iostream>
#include <functional>
using namespace std;
class Processor
{
private:
function<void(string)> callback;
public:
void SetCallback(function<void(string)> cb)
{
callback = cb;
}
void Process()
{
if (callback)
callback("Processing Complete");
}
};
int main()
{
Processor processor;
// 使用Lambda表达式作为函数对象
processor.SetCallback([](string message) {
cout << message << endl;
});
processor.Process(); // 输出 Processing Complete
return 0;
}
6. 总结
- C#的委托和事件更适合面向对象的开发,尤其是在事件驱动的场景中(如GUI编程、异步任务)。
- C++的函数对象则更加灵活,尤其在需要保存状态或与STL算法结合使用时表现出色。
- 两者的核心思想都是封装行为,但在具体实现和应用场景上各有侧重。选择哪种方式取决于语言特性和实际需求。
一些简单的示例
委托(Delegate)和事件(Event)是C#中非常强大的机制,尤其是在实现回调、观察者模式、异步编程等场景时。以下是一些实用的例子,展示如何在实际开发中使用委托和事件。
1. 委托的实用例子
1.1 回调函数
假设我们有一个耗时的任务(例如文件下载),我们希望在任务完成时通知调用者。可以使用委托来实现回调。
using System;
public delegate void DownloadCompletedHandler(string fileName);
public class FileDownloader
{
public DownloadCompletedHandler OnDownloadCompleted;
public void DownloadFile(string fileName)
{
Console.WriteLine($"Downloading {fileName}...");
// 模拟下载过程
System.Threading.Thread.Sleep(2000);
Console.WriteLine($"{fileName} downloaded.");
// 通知任务完成
OnDownloadCompleted?.Invoke(fileName);
}
}
public class Program
{
public static void Main()
{
FileDownloader downloader = new FileDownloader();
// 绑定回调方法
downloader.OnDownloadCompleted += fileName =>
{
Console.WriteLine($"Callback: {fileName} is ready to use.");
};
// 开始下载
downloader.DownloadFile("example.txt");
}
}
输出:
Downloading example.txt...
example.txt downloaded.
Callback: example.txt is ready to use.
1.2 多播委托
多播委托允许一个委托同时调用多个方法。以下是一个简单的日志记录系统的例子。
using System;
public delegate void LogHandler(string message);
public class Logger
{
public LogHandler Log;
public void RecordLog(string message)
{
Console.WriteLine("Logging...");
Log?.Invoke(message);
}
}
public class Program
{
public static void Main()
{
Logger logger = new Logger();
// 添加多个日志处理方法
logger.Log += ConsoleLogger;
logger.Log += FileLogger;
// 记录日志
logger.RecordLog("System started.");
}
public static void ConsoleLogger(string message)
{
Console.WriteLine($"[Console] {message}");
}
public static void FileLogger(string message)
{
// 模拟写入文件
Console.WriteLine($"[File] {message}");
}
}
输出:
Logging...
[Console] System started.
[File] System started.
2. 事件的实用例子
2.1 发布-订阅模式
事件通常用于实现发布-订阅模式。以下是一个简单的股票价格监控系统的例子。
using System;
public class Stock
{
// 定义事件
public event EventHandler<PriceChangedEventArgs> PriceChanged;
private decimal _price;
public decimal Price
{
get => _price;
set
{
if (_price != value)
{
_price = value;
// 触发事件
PriceChanged?.Invoke(this, new PriceChangedEventArgs(_price));
}
}
}
}
// 自定义事件参数类
public class PriceChangedEventArgs : EventArgs
{
public decimal NewPrice { get; }
public PriceChangedEventArgs(decimal newPrice)
{
NewPrice = newPrice;
}
}
public class Investor
{
public string Name { get; }
public Investor(string name)
{
Name = name;
}
public void OnPriceChanged(object sender, PriceChangedEventArgs e)
{
Console.WriteLine($"{Name} received notification: New price is {e.NewPrice:C}");
}
}
public class Program
{
public static void Main()
{
Stock stock = new Stock();
Investor investor1 = new Investor("Alice");
Investor investor2 = new Investor("Bob");
// 订阅事件
stock.PriceChanged += investor1.OnPriceChanged;
stock.PriceChanged += investor2.OnPriceChanged;
// 修改股票价格
stock.Price = 100.50m;
stock.Price = 102.75m;
}
}
输出:
Alice received notification: New price is $100.50
Bob received notification: New price is $100.50
Alice received notification: New price is $102.75
Bob received notification: New price is $102.75
2.2 异步事件处理
在某些情况下,事件处理可能需要异步执行。以下是一个简单的例子,模拟用户登录后触发异步通知。
using System;
using System.Threading.Tasks;
public class UserLogin
{
public event EventHandler<string> LoginSuccessful;
public async Task LoginAsync(string username)
{
Console.WriteLine($"Logging in as {username}...");
await Task.Delay(2000); // 模拟登录延迟
Console.WriteLine($"Login successful for {username}.");
// 异步触发事件
LoginSuccessful?.Invoke(this, username);
}
}
public class NotificationService
{
public void OnLoginSuccessful(object sender, string username)
{
Console.WriteLine($"Sending welcome email to {username}...");
}
}
public class Program
{
public static async Task Main()
{
UserLogin userLogin = new UserLogin();
NotificationService notifier = new NotificationService();
// 订阅事件
userLogin.LoginSuccessful += notifier.OnLoginSuccessful;
// 模拟用户登录
await userLogin.LoginAsync("JohnDoe");
}
}
输出:
Logging in as JohnDoe...
Login successful for JohnDoe.
Sending welcome email to JohnDoe...
2.3 使用EventHandler<T>
简化事件定义
C# 提供了泛型EventHandler<T>
,可以简化事件的定义和使用。以下是一个按钮点击事件的例子。
using System;
public class Button
{
// 使用泛型EventHandler<T>
public event EventHandler<ButtonClickEventArgs> Clicked;
public void Click()
{
Console.WriteLine("Button clicked!");
Clicked?.Invoke(this, new ButtonClickEventArgs(DateTime.Now));
}
}
// 自定义事件参数类
public class ButtonClickEventArgs : EventArgs
{
public DateTime ClickTime { get; }
public ButtonClickEventArgs(DateTime clickTime)
{
ClickTime = clickTime;
}
}
public class Program
{
public static void Main()
{
Button button = new Button();
// 订阅事件
button.Clicked += (sender, e) =>
{
Console.WriteLine($"Button was clicked at {e.ClickTime}");
};
// 模拟按钮点击
button.Click();
}
}
输出:
Button clicked!
Button was clicked at 10/10/2023 14:30:00
3. 总结
- 委托适合用于封装方法引用,支持回调和多播。
- 事件基于委托,主要用于实现发布-订阅模式,常见于GUI编程、异步任务和状态变化通知。
- 上述例子涵盖了常见的应用场景,包括回调、日志记录、股票价格监控、异步事件处理和按钮点击事件。
通过这些例子,你可以更好地理解如何在实际项目中使用委托和事件来构建灵活且可扩展的系统。
与异步的结合
委托(Delegate)和事件(Event)与异步编程之间有着紧密的联系,尤其是在现代C#开发中。它们为异步编程提供了灵活且强大的机制,使得开发者能够以清晰、优雅的方式处理异步任务的结果或状态变化。
以下从几个方面详细分析它们之间的联系:
1. 委托与异步编程
1.1 回调机制
在异步编程中,一个常见的需求是:当某个异步操作完成时,通知调用者执行后续逻辑。委托可以作为回调函数的载体,用于封装需要在异步操作完成后执行的代码。
示例:使用委托处理异步任务结果
using System;
using System.Threading.Tasks;
public delegate void TaskCompletedHandler(string result);
public class AsyncTaskRunner
{
public TaskCompletedHandler OnTaskCompleted;
public async Task RunAsync()
{
Console.WriteLine("Starting asynchronous task...");
string result = await Task.Delay(2000).ContinueWith(_ => "Task Result");
// 异步任务完成后触发回调
OnTaskCompleted?.Invoke(result);
}
}
public class Program
{
public static async Task Main()
{
AsyncTaskRunner runner = new AsyncTaskRunner();
// 绑定回调方法
runner.OnTaskCompleted += result =>
{
Console.WriteLine($"Task completed with result: {result}");
};
// 启动异步任务
await runner.RunAsync();
}
}
输出:
Starting asynchronous task...
Task completed with result: Task Result
在这个例子中:
OnTaskCompleted
是一个委托,用于定义异步任务完成后的回调逻辑。- 当异步任务完成后,通过调用委托来通知调用者。
1.2 Func 和 Action
C# 提供了内置的泛型委托 Func<T>
和 Action<T>
,它们可以直接用于异步编程中的回调逻辑。
示例:使用 Func
处理异步任务
using System;
using System.Threading.Tasks;
public class AsyncCalculator
{
public async Task<int> CalculateAsync(Func<int, int, int> operation, int a, int b)
{
Console.WriteLine("Calculating asynchronously...");
await Task.Delay(1000); // 模拟耗时计算
return operation(a, b);
}
}
public class Program
{
public static async Task Main()
{
AsyncCalculator calculator = new AsyncCalculator();
// 定义异步计算逻辑
int result = await calculator.CalculateAsync((x, y) => x + y, 5, 10);
Console.WriteLine($"Result: {result}");
}
}
输出:
Calculating asynchronously...
Result: 15
在这个例子中:
Func<int, int, int>
是一个委托,表示接受两个整数参数并返回一个整数的方法。- 异步方法
CalculateAsync
使用该委托来封装具体的计算逻辑。
2. 事件与异步编程
2.1 事件驱动的异步通知
事件通常用于实现发布-订阅模式,在异步编程中,它可以用来通知订阅者某个异步操作的状态变化或完成情况。
示例:使用事件通知异步任务完成
using System;
using System.Threading.Tasks;
public class AsyncTaskManager
{
// 定义事件
public event EventHandler<string> TaskCompleted;
public async Task ExecuteAsync()
{
Console.WriteLine("Executing asynchronous task...");
await Task.Delay(2000); // 模拟耗时操作
// 触发事件
TaskCompleted?.Invoke(this, "Task Completed Successfully");
}
}
public class Program
{
public static async Task Main()
{
AsyncTaskManager manager = new AsyncTaskManager();
// 订阅事件
manager.TaskCompleted += (sender, message) =>
{
Console.WriteLine($"Notification: {message}");
};
// 执行异步任务
await manager.ExecuteAsync();
}
}
输出:
Executing asynchronous task...
Notification: Task Completed Successfully
在这个例子中:
TaskCompleted
是一个事件,用于通知订阅者异步任务已完成。- 事件的触发点是在异步操作完成后,确保调用者能够及时收到通知。
2.2 异步事件处理
在某些场景下,事件处理本身可能需要异步执行。C# 支持异步事件处理程序,可以通过 async void
或 async Task
方法来实现。
示例:异步事件处理
using System;
using System.Threading.Tasks;
public class NotificationService
{
public event EventHandler<string> Notify;
public async Task SendNotificationAsync(string message)
{
Console.WriteLine("Sending notification...");
await Task.Delay(1000); // 模拟异步发送
// 触发事件
Notify?.Invoke(this, message);
}
}
public class Program
{
public static async Task Main()
{
NotificationService service = new NotificationService();
// 订阅事件
service.Notify += async (sender, message) =>
{
await Task.Delay(500); // 模拟异步处理
Console.WriteLine($"Received notification: {message}");
};
// 发送通知
await service.SendNotificationAsync("Hello, World!");
}
}
输出:
Sending notification...
Received notification: Hello, World!
在这个例子中:
- 事件处理程序是一个异步方法,使用
async
关键字定义。 - 这种方式适用于需要在事件处理中执行耗时操作的场景。
3. 委托与事件在异步编程中的优势
3.1 解耦
- 委托允许将异步任务的完成逻辑与任务本身解耦。例如,调用者可以自由定义回调逻辑,而无需修改异步方法的实现。
- 事件进一步增强了这种解耦能力,因为它允许多个订阅者独立响应同一个异步操作的结果。
3.2 灵活性
- 委托支持多播(Multicast),可以在异步任务完成后同时调用多个回调方法。
- 事件通过发布-订阅模式,支持动态添加或移除订阅者,非常适合复杂的异步场景。
3.3 可读性
- 使用委托和事件可以让异步代码更加清晰,尤其是当异步操作涉及多个步骤或状态变化时。
4. 实际应用场景
4.1 GUI 编程
在GUI框架(如WPF或WinForms)中,事件常用于处理用户交互(如按钮点击)。这些事件通常是异步触发的,因为用户操作可能发生在任意时刻。
4.2 Web API 调用
在调用远程API时,可以使用委托或事件来处理异步响应。例如:
- 使用委托定义成功或失败的回调逻辑。
- 使用事件通知订阅者API调用的结果。
4.3 数据流处理
在数据流处理(如Rx.NET或SignalR)中,事件常用于实时推送数据更新。这些数据更新通常是异步生成的。
5. 总结
- 委托和事件是异步编程的重要工具,它们通过回调机制和发布-订阅模式,帮助开发者优雅地处理异步任务的结果或状态变化。
- 委托更适合简单的回调场景,而事件则适合复杂的多订阅者场景。
- 在实际开发中,合理使用委托和事件,可以让异步代码更加清晰、灵活且易于维护。
希望这些内容能帮助你更好地理解委托、事件与异步编程之间的关系!
1. 原例子中的同步实现
1.1 文件下载回调(同步)
public class FileDownloader
{
public DownloadCompletedHandler OnDownloadCompleted;
public void DownloadFile(string fileName)
{
Console.WriteLine($"Downloading {fileName}...");
System.Threading.Thread.Sleep(2000); // 同步阻塞
OnDownloadCompleted?.Invoke(fileName);
}
}
- 问题:使用
Thread.Sleep
模拟下载,会阻塞当前线程。 - 改进:改用
Task.Delay
和async/await
实现异步。
2. 异步版本的改写
2.1 异步文件下载(使用 async/await
)
using System;
using System.Threading.Tasks;
public delegate void DownloadCompletedHandler(string fileName);
public class FileDownloader
{
public DownloadCompletedHandler OnDownloadCompleted;
public async Task DownloadFileAsync(string fileName)
{
Console.WriteLine($"Downloading {fileName}...");
await Task.Delay(2000); // 异步等待,不阻塞线程
Console.WriteLine($"{fileName} downloaded.");
OnDownloadCompleted?.Invoke(fileName);
}
}
public class Program
{
public static async Task Main()
{
FileDownloader downloader = new FileDownloader();
downloader.OnDownloadCompleted += fileName =>
{
Console.WriteLine($"Callback: {fileName} is ready to use.");
};
await downloader.DownloadFileAsync("example.txt");
}
}
输出:
Downloading example.txt...
example.txt downloaded.
Callback: example.txt is ready to use.
2.2 异步股票价格监控
原例子中的 Price
属性是同步的,但可以通过异步事件触发:
using System;
using System.Threading.Tasks;
public class Stock
{
public event EventHandler<PriceChangedEventArgs> PriceChanged;
private decimal _price;
public decimal Price
{
get => _price;
set
{
if (_price != value)
{
_price = value;
PriceChanged?.Invoke(this, new PriceChangedEventArgs(_price));
}
}
}
// 异步模拟价格变化
public async Task SimulatePriceChangeAsync()
{
for (int i = 0; i < 3; i++)
{
await Task.Delay(1000); // 异步等待
Price += 10.5m; // 触发PriceChanged事件
}
}
}
public class Program
{
public static async Task Main()
{
Stock stock = new Stock();
stock.PriceChanged += (sender, e) =>
{
Console.WriteLine($"Price updated to {e.NewPrice:C}");
};
await stock.SimulatePriceChangeAsync();
}
}
输出:
Price updated to $10.50
Price updated to $21.00
Price updated to $31.50
3. 原例子中的潜在异步场景
3.1 事件处理本身是异步的
即使事件触发是同步的,事件处理方法也可以是异步的。例如:
public class NotificationService
{
public event EventHandler<string> Notify;
public void SendNotification(string message)
{
Console.WriteLine("Sending notification...");
Notify?.Invoke(this, message);
}
}
public class Program
{
public static void Main()
{
NotificationService service = new NotificationService();
// 异步事件处理
service.Notify += async (sender, message) =>
{
await Task.Delay(500); // 模拟异步操作
Console.WriteLine($"Received: {message}");
};
service.SendNotification("Hello, World!");
Console.ReadLine(); // 防止主线程退出
}
}
输出:
Sending notification...
(等待500ms后)
Received: Hello, World!
4. 总结
- 原例子中的委托和事件是同步的,但可以通过以下方式实现异步:
- 使用
async/await
和Task
改写耗时操作。 - 在事件处理方法中使用异步逻辑(
async void
或async Task
)。
- 使用
- 异步的优势:
- 避免阻塞主线程(例如在GUI应用中保持界面响应)。
- 提高资源利用率(例如在服务器端处理高并发请求)。
通过结合委托、事件和异步编程,可以构建高效且可维护的异步系统。