引言
C# 中的两个强大特性:委托和事件。它们就像瑞士军刀一样,小巧玲珑却功能强大,在各种编程场景中都能发挥重要作用。
无论你是刚接触 C# 的新手,还是有一定经验的开发者,理解委托和事件都是掌握 C# 核心编程思想的关键。本文将通过通俗易懂的语言和实用的代码示例,帮助你轻松掌握这一技术。
委托:方法的「容器」
1. 什么是委托?
简单来说,委托就是一个「方法容器」,它可以存储对方法的引用。想象一下,你有一个盒子,里面可以放各种形状相同的工具,委托就像这个盒子,它可以装下所有签名相同的方法。
// 定义一个委托类型
delegate void MyDelegate(string message);
class Program
{
static void Main()
{
// 创建委托实例并指向一个方法
MyDelegate del = new MyDelegate(PrintMessage);
// 调用委托
del("Hello, Delegate!");
}
static void PrintMessage(string message)
{
Console.WriteLine(message);
}
}2. 委托的特点
类型安全:编译时会检查方法签名是否匹配,避免运行时错误 • 多播能力:一个委托可以指向多个方法,形成调用链 • 可传递性:委托可以作为参数传递给其他方法 • 返回值:委托可以有返回值,多播时只返回最后一个方法的结果
3. 泛型委托:更简洁的写法
C# 提供了一些预定义的泛型委托,让代码更简洁:
• Action:无返回值的委托• Func:有返回值的委托• Predicate:返回布尔值的委托
// Action 委托:无返回值
Action<string> action = (message) => Console.WriteLine(message);
action("Hello, Action!");
// Func 委托:有返回值
Func<int, int, int> add = (a, b) => a + b;
int result = add(10, 20);
Console.WriteLine(result);
// Predicate 委托:返回布尔值
Predicate<int> isEven = (num) => num % 2 == 0;
bool result = isEven(4);
Console.WriteLine(result);事件:对象间的「消息传递员」
1. 什么是事件?
事件是基于委托的一种机制,它实现了「发布-订阅」模式。想象一下,你订阅了一份报纸,当有新内容时,报社就会把报纸送到你手上。事件就是这样:当某个对象发生特定事情时,它会通知所有订阅了这个事件的对象。
class Publisher
{
// 定义事件
public event Action<string> MessagePublished;
public void Publish(string message)
{
// 触发事件
MessagePublished?.Invoke(message);
}
}
class Subscriber
{
public Subscriber(Publisher publisher)
{
// 订阅事件
publisher.MessagePublished += OnMessagePublished;
}
private void OnMessagePublished(string message)
{
Console.WriteLine($"Received: {message}");
}
}
class Program
{
static void Main()
{
Publisher publisher = new Publisher();
Subscriber subscriber = new Subscriber(publisher);
publisher.Publish("Hello, Event!");
}
}2. 事件的特点
• 封装性:事件只能在定义它的类内部触发,外部代码无法直接触发 • 订阅/取消订阅:使用 +=订阅事件,使用-=取消订阅• 线程安全:事件的触发是线程安全的 • 委托基础:事件本质上是对委托的封装,提供了更安全的访问控制
委托与事件的关系
1. 事件是委托的「安全封装」
事件可以看作是委托的一个特例,它添加了额外的访问控制:
• 外部代码只能订阅或取消订阅事件,不能直接触发它 • 外部代码不能直接修改事件的委托链
2. 委托与事件的区别
实际应用场景
1. 回调函数:任务完成后的「通知」
委托最基本的用途是作为回调函数,当一个任务完成后,自动执行指定的方法:
void ProcessData(string data, Action<string> callback)
{
// 处理数据
string result = data.ToUpper();
// 调用回调
callback(result);
}
// 使用
ProcessData("hello", (result) => Console.WriteLine(result));2. 事件驱动编程:响应用户操作
在 GUI 编程中,事件用于处理用户交互,比如按钮点击、鼠标移动等:
// Windows Forms 示例
button1.Click += (sender, e) => {
MessageBox.Show("Button clicked!");
};3. 观察者模式:状态变化的「广播」
事件是实现观察者模式的理想选择,当一个对象的状态发生变化时,通知所有关注它的对象:
class Stock
{
public event Action<decimal> PriceChanged;
private decimal price;
public decimal Price
{
get { return price; }
set
{
if (price != value)
{
price = value;
PriceChanged?.Invoke(price);
}
}
}
}
class Investor
{
public string Name { get; set; }
public Investor(string name, Stock stock)
{
Name = name;
stock.PriceChanged += OnPriceChanged;
}
private void OnPriceChanged(decimal price)
{
Console.WriteLine($"{Name} notified: Stock price is now {price}");
}
}高级用法
1. 异步委托:不阻塞主线程
委托可以用于异步操作,让耗时任务在后台执行,不阻塞主线程:
Func<int, int, int> add = (a, b) => {
Thread.Sleep(1000); // 模拟耗时操作
return a + b;
};
// 异步调用
IAsyncResult result = add.BeginInvoke(10, 20, null, null);
// 做其他事情
Console.WriteLine("Doing other work...");
// 获取结果
int sum = add.EndInvoke(result);
Console.WriteLine($"Sum: {sum}");2. 委托的组合与移除:灵活管理方法链
Action<string> action1 = (msg) => Console.WriteLine($"Action1: {msg}");
Action<string> action2 = (msg) => Console.WriteLine($"Action2: {msg}");
// 组合委托
Action<string> combined = action1 + action2;
combined("Hello");
// 移除委托
combined -= action1;
combined("Hello");3. 自定义事件访问器:更精细的控制
对于复杂场景,我们可以自定义事件的访问器,实现更精细的控制:
class CustomEventPublisher
{
private Action<string> messageHandlers;
public event Action<string> MessagePublished
{
add
{
Console.WriteLine("A subscriber added");
messageHandlers += value;
}
remove
{
Console.WriteLine("A subscriber removed");
messageHandlers -= value;
}
}
public void Publish(string message)
{
messageHandlers?.Invoke(message);
}
}最佳实践
1. 委托使用建议
• 对于简单的回调,优先使用 Action或Func泛型委托• 对于复杂的场景,定义专用的委托类型,提高代码可读性 • 避免在委托链中包含耗时操作,以免影响性能
2. 事件使用建议
• 始终使用 EventHandler或EventHandler<TEventArgs>作为事件委托类型• 遵循 .NET 事件命名约定: OnEventName作为触发方法,EventName作为事件名称• 在触发事件前检查是否为 null(使用 ?.操作符)• 避免在事件处理程序中抛出异常,以免影响其他订阅者
3. 性能考虑
• 对于高频事件,考虑使用弱事件模式,避免内存泄漏 • 对于大型委托链,考虑使用事件聚合器模式,提高管理效率 • 避免在事件处理程序中执行耗时操作,考虑使用异步处理
常见陷阱
1. 内存泄漏:不要让对象「赖着不走」
如果订阅者对象的生命周期比发布者长,可能会导致内存泄漏:
// 不好的做法
public class BadSubscriber
{
public BadSubscriber(Publisher publisher)
{
// 匿名方法捕获了 this,导致 publisher 持有对 subscriber 的引用
publisher.MessagePublished += (message) => {
Console.WriteLine($"{this.GetType().Name} received: {message}");
};
}
}
// 好的做法:使用弱引用或明确取消订阅
public class GoodSubscriber : IDisposable
{
private Publisher publisher;
public GoodSubscriber(Publisher publisher)
{
this.publisher = publisher;
publisher.MessagePublished += OnMessagePublished;
}
private void OnMessagePublished(string message)
{
Console.WriteLine($"{GetType().Name} received: {message}");
}
public void Dispose()
{
publisher.MessagePublished -= OnMessagePublished;
}
}2. 线程安全:多线程环境下的注意事项
在多线程环境中,事件触发需要注意线程安全:
// 线程安全的事件触发
public event Action<string> MessagePublished;
protected virtual void OnMessagePublished(string message)
{
// 复制到局部变量,避免在触发过程中委托被修改
Action<string> handler = MessagePublished;
handler?.Invoke(message);
}代码示例
完整的委托与事件示例
using System;
namespace DelegateAndEventSamples
{
// 基本委托与事件示例相关类型
delegate void MessageHandler(string message);
class Publisher
{
// 定义事件
public event MessageHandler? MessageSent;
public void SendMessage(string message)
{
Console.WriteLine($"[Publisher] 发送消息: {message}");
// 触发事件
OnMessageSent(message);
}
protected virtual void OnMessageSent(string message)
{
Console.WriteLine("[Publisher] 触发 MessageSent 事件");
MessageSent?.Invoke(message);
}
}
class Subscriber
{
private string name;
public Subscriber(string name, Publisher publisher)
{
this.name = name;
// 订阅事件
Console.WriteLine($"[Subscriber] {name} 订阅了 MessageSent 事件");
publisher.MessageSent += ReceiveMessage;
}
private void ReceiveMessage(string message)
{
Console.WriteLine($"[Subscriber] {name} 收到消息: {message}");
}
}
class Program
{
static void Main(string[] args)
{
// 创建发布者
Publisher publisher = new Publisher();
// 创建订阅者
Subscriber subscriber1 = new Subscriber("Subscriber 1", publisher);
Subscriber subscriber2 = new Subscriber("Subscriber 2", publisher);
// 发送消息
publisher.SendMessage("Hello, World!");
Console.ReadLine();
}
}
}泛型委托示例
using System;
namespace DelegateAndEventSamples
{
class Program
{
static void Main(string[] args)
{
// Action 委托:无返回值
Action<string> printAction = (message) => Console.WriteLine($" 执行 Action: {message}");
printAction("Hello from Action!");
// Func 委托:有返回值
Func<int, int, int> addFunc = (a, b) =>
{
int result = a + b;
Console.WriteLine($" 执行 Func: {a} + {b} = {result}");
return result;
};
int sum = addFunc(10, 20);
// Predicate 委托:返回布尔值
Predicate<int> isPositive = (num) =>
{
bool result = num > 0;
Console.WriteLine($" 执行 Predicate: {num} 是正数吗? {result}");
return result;
};
bool result = isPositive(5);
Console.ReadLine();
}
}
}事件聚合器示例
using System;
using System.Collections.Generic;
namespace DelegateAndEventSamples
{
class EventAggregator
{
private readonly Dictionary<Type, List<Delegate>> eventHandlers = new Dictionary<Type, List<Delegate>>();
public void Subscribe<T>(Action<T> handler)
{
Type eventType = typeof(T);
if (!eventHandlers.ContainsKey(eventType))
{
eventHandlers[eventType] = new List<Delegate>();
Console.WriteLine($"[EventAggregator] 为 {eventType.Name} 类型创建事件处理器列表");
}
eventHandlers[eventType].Add(handler);
Console.WriteLine($"[EventAggregator] 订阅 {eventType.Name} 类型的事件");
}
public void Unsubscribe<T>(Action<T> handler)
{
Type eventType = typeof(T);
if (eventHandlers.ContainsKey(eventType))
{
eventHandlers[eventType].Remove(handler);
Console.WriteLine($"[EventAggregator] 取消订阅 {eventType.Name} 类型的事件");
}
}
public void Publish<T>(T eventData)
{
Type eventType = typeof(T);
Console.WriteLine($"[EventAggregator] 发布 {eventType.Name} 类型的事件");
if (eventHandlers.ContainsKey(eventType))
{
Console.WriteLine($"[EventAggregator] 找到 {eventHandlers[eventType].Count} 个事件处理器");
foreach (Action<T> handler in eventHandlers[eventType])
{
handler(eventData);
}
}
else
{
Console.WriteLine($"[EventAggregator] 没有找到 {eventType.Name} 类型的事件处理器");
}
}
}
// 事件数据类
class MessageEvent
{
public string Message { get; set; } = string.Empty;
}
class Program
{
static void Main(string[] args)
{
EventAggregator aggregator = new EventAggregator();
// 订阅事件
aggregator.Subscribe<MessageEvent>((e) => Console.WriteLine($" 事件处理器: 收到消息: {e.Message}"));
// 发布事件
aggregator.Publish(new MessageEvent { Message = "Hello from EventAggregator!" });
Console.ReadLine();
}
}
}整合示例程序
我们还创建了一个整合了所有示例的控制台应用程序,使用选项式菜单系统让你可以方便地运行和查看所有示例:
using System;
using System.Collections.Generic;
namespace DelegateAndEventSamples
{
class Program
{
static void Main(string[] args)
{
bool exit = false;
while (!exit)
{
Console.Clear();
Console.WriteLine("==================================");
Console.WriteLine("C# 委托与事件示例程序");
Console.WriteLine("==================================");
Console.WriteLine("1. 基本委托与事件示例");
Console.WriteLine("2. 泛型委托示例");
Console.WriteLine("3. 事件聚合器示例");
Console.WriteLine("4. 退出程序");
Console.WriteLine("==================================");
Console.Write("请选择示例编号: ");
string input = Console.ReadLine();
Console.WriteLine();
switch (input)
{
case "1":
RunDelegateAndEventExample();
break;
case "2":
RunGenericDelegateExample();
break;
case "3":
RunEventAggregatorExample();
break;
case "4":
exit = true;
break;
default:
Console.WriteLine("无效的选择,请重新输入。");
break;
}
if (!exit)
{
Console.WriteLine("\n按回车键返回菜单...");
Console.ReadLine();
}
}
Console.WriteLine("程序已退出。");
}
}
}示例运行效果如下:
委托和事件是 C# 中非常强大的特性,它们为我们提供了一种灵活的方式来实现回调、通知和事件驱动编程。就像瑞士军刀一样,它们在各种场景下都能发挥重要作用,从简单的回调到复杂的事件系统,都能轻松应对。 在实际开发中,合理使用委托和事件可以让你的代码更加灵活、可维护,同时也能更好地实现模块化和松耦合的设计。 希望本文对你有所帮助,祝你在 C# 编程之路上越走越远!总结