C# 委托与事件:delegate 与 event 关键字
在 C# 编程中,委托(Delegate)和事件(Event)是两个紧密相关却常被混淆的概念。委托是一种类型安全的函数引用机制,而事件则是基于委托的发布-订阅模式实现。理解这两个关键字的区别与配合方式,是掌握 C# 高级编程的关键一步。
1. 委托的本质:指向方法的类型
委托在本质上是一个类型安全的回调机制。它可以像普通变量一样被声明、赋值和传递,但它的值是一个或多个方法的引用。
1.1 声明与定义委托类型
使用 delegate 关键字声明委托类型时,需要指定它可以引用的方法签名。
// 声明一个委托类型:接受两个int参数,返回int结果
public delegate int CalculatorDelegate(int a, int b);
// 声明一个委托类型:没有参数,没有返回值
public delegate void NotificationDelegate(string message);
委托的签名包括参数列表和返回类型。只有完全匹配签名的方法才能被赋值给该委托类型的变量。
1.2 创建委托实例并调用
声明委托类型后,可以创建该类型的实例,将方法作为参数传递进去。
public class MathOperations
{
public static int Add(int a, int b) => a + b;
public static int Multiply(int a, int b) => a * b;
}
class Program
{
static void Main()
{
// 创建委托实例,引用静态方法
CalculatorDelegate calc = MathOperations.Add;
// 通过委托调用方法
int result = calc(10, 20); // 结果为 30
Console.WriteLine(result);
}
}
委托实例的调用语法与普通方法调用完全相同,编译器会自动将调用转发到引用的目标方法。
1.3 多播委托
委托支持组合多个方法,形成多播委托。当调用多播委托时,会按添加顺序依次执行所有引用的方法。
public delegate void LogDelegate(string message);
public class Logger
{
public static void LogToConsole(string msg)
{
Console.WriteLine($"[Console] {msg}");
}
public static void LogToFile(string msg)
{
Console.WriteLine($"[File] {msg}");
}
}
class Program
{
static void Main()
{
LogDelegate log = Logger.LogToConsole;
log += Logger.LogToFile; // 添加第二个方法
log("操作完成"); // 两个方法都会被调用
}
}
使用 += 运算符可以向委托添加新方法,使用 -= 运算符可以移除方法。如果委托有返回值,多播调用只会返回最后一个方法的返回值。
2. 事件的实现:封装后的委托
事件是一种特殊的委托,它在委托的基础上增加了访问修饰限制,确保订阅者只能订阅或取消订阅,无法直接触发或修改委托链。
2.1 事件的声明语法
使用 event 关键字声明事件成员,其类型必须是一个委托类型。
public class Publisher
{
// 声明事件成员
public event EventHandler<OrderEventArgs> OrderProcessed;
public void ProcessOrder(Order order)
{
// 处理订单逻辑
order.Status = "Processed";
// 触发事件:只能在类内部进行
OrderProcessed?.Invoke(this, new OrderEventArgs(order));
}
}
EventHandler 是 .NET 框架提供的标准委托类型,泛型版本 EventHandler<T> 是最常用的事件模式。事件的触发只能发生在声明它的类内部,这是事件与普通委托的关键区别。
2.2 订阅事件
使用 += 运算符为事件添加订阅者,即指定当事件触发时应该执行的方法。
public class Subscriber
{
public void HandleOrder(object sender, OrderEventArgs e)
{
Console.WriteLine($"收到订单通知:{e.Order.Id}");
}
}
class Program
{
static void Main()
{
Publisher publisher = new Publisher();
Subscriber subscriber = new Subscriber();
// 订阅事件
publisher.OrderProcessed += subscriber.HandleOrder;
// 触发事件
publisher.ProcessOrder(new Order { Id = "ORD-001" });
}
}
```
订阅事件时,提供的方法会自动成为事件的处理器。当事件被触发时,订阅的方法会被执行。
### 2.3 取消订阅
使用 `-=` 运算符可以从事件中移除订阅者。及时取消不再需要的事件订阅,可以防止内存泄漏。
```csharp
// 取消订阅
publisher.OrderProcessed -= subscriber.HandleOrder;
```
如果订阅者是匿名方法或 lambda 表达式,取消订阅时需要持有对该匿名方法的引用。
---
## 3. 委托与事件的关键区别
理解委托与事件的区别对于写出高质量的 C# 代码至关重要。
| 特性 | 委托 | 事件 |
|------|------|------|
| **访问权限** | 可以在类外部直接调用和赋值 | 只能在类内部触发,外部只能订阅或取消 |
| **封装性** | 完全暴露,无保护 | 封装了委托,限制了外部操作 |
| **使用场景** | 回调函数、通用算法参数 | 发布-订阅模式、通知机制 |
| **声明位置** | 可以是类成员也可以是顶级类型 | 只能是类的成员 |
事件实际上是对委托的封装,它隐藏了 `Invoke()` 方法和赋值操作(`=`、`+=`、`-=` 中的赋值语义),只暴露订阅和取消订阅的能力。这种设计模式确保了事件的发起者完全控制事件的触发时机和方式。
---
## 4. 自定义事件参数
当需要传递额外信息给事件处理器时,可以创建继承自 `EventArgs` 的自定义参数类。
```csharp
public class OrderEventArgs : EventArgs
{
public Order Order { get; }
public DateTime ProcessedAt { get; }
public OrderEventArgs(Order order)
{
Order = order;
ProcessedAt = DateTime.Now;
}
}
public class Publisher
{
// 使用自定义事件参数类型
public event EventHandler<OrderEventArgs> OrderProcessed;
protected virtual void OnOrderProcessed(Order order)
{
// 空引用检查后触发
OrderProcessed?.Invoke(this, new OrderEventArgs(order));
}
}
```
`OnOrderProcessed` 这样的受保护虚方法是一种推荐模式,它允许子类在触发事件前执行额外逻辑或进行控制。
---
## 5. 实际应用场景
### 5.1 按钮点击事件
WinForms 或 WPF 中的按钮点击是最常见的事件应用场景。
```csharp
public class Button
{
public event EventHandler Click;
public void SimulateClick()
{
// 模拟用户点击
OnClick(EventArgs.Empty);
}
protected virtual void OnClick(EventArgs e)
{
Click?.Invoke(this, e);
}
}
// 使用方
Button btn = new Button();
btn.Click += (s, e) => Console.WriteLine("按钮被点击了");
btn.SimulateClick();
```
### 5.2 跨组件通信
在分层架构中,事件常用于实现组件间的松耦合通信。
```csharp
public class OrderService
{
public event EventHandler<OrderPlacedEventArgs> OrderPlaced;
public void PlaceOrder(Order order)
{
// 订单处理逻辑
// 触发事件通知其他组件
OnOrderPlaced(new OrderPlacedEventArgs(order));
}
}
public class InventoryService
{
public InventoryService(OrderService orderService)
{
// 订阅事件
orderService.OrderPlaced += ReduceInventory;
}
private void ReduceInventory(object sender, OrderPlacedEventArgs e)
{
// 扣减库存逻辑
Console.WriteLine($"订单 {e.OrderId} 已扣减库存");
}
}
通过事件机制,OrderService 无需直接依赖 InventoryService,两者的生命周期可以独立管理,符合依赖倒置原则。
6. 最佳实践总结
使用 Func 和 Action 泛型委托:对于大多数场景,直接使用框架提供的 Func<T, TResult> 和 Action<T> 泛型委托,无需自定义委托类型,可以减少代码冗余。
使用 ?.Invoke() 进行空值检查:触发事件前始终使用空条件运算符,避免在没有任何订阅者时抛出 NullReferenceException。
线程安全考虑:如果事件可能在多线程环境下被订阅或触发,需要在订阅操作时加锁,或使用 Volatile.Write 和 Thread.MemoryBarrier 确保可见性。
事件命名规范:事件名称应该是现在时态的动词或动词短语,如 Click、Processing、Changed,能够清晰表达事件发生的时机。

暂无评论,快来抢沙发吧!