文章目录

C# 委托与事件:delegate 与 event 关键字

发布于 2026-04-05 04:24:56 · 浏览 17 次 · 评论 0 条

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. 最佳实践总结

使用 FuncAction 泛型委托:对于大多数场景,直接使用框架提供的 Func<T, TResult>Action<T> 泛型委托,无需自定义委托类型,可以减少代码冗余。

使用 ?.Invoke() 进行空值检查:触发事件前始终使用空条件运算符,避免在没有任何订阅者时抛出 NullReferenceException

线程安全考虑:如果事件可能在多线程环境下被订阅或触发,需要在订阅操作时加锁,或使用 Volatile.WriteThread.MemoryBarrier 确保可见性。

事件命名规范:事件名称应该是现在时态的动词或动词短语,如 ClickProcessingChanged,能够清晰表达事件发生的时机。

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文