一、委托与事件:C#中的通信基石

在C#中,委托和事件就像现实生活中的"中介"和"通知栏"。委托定义了方法的签名,而事件则基于委托实现了一种发布-订阅模式。我们先看一个简单的委托示例:

// 定义一个委托类型
public delegate void MessageHandler(string message);

class Program
{
    // 使用委托作为参数
    static void ProcessMessage(MessageHandler handler)
    {
        handler("Hello from delegate!");
    }

    static void Main()
    {
        // 实例化委托并绑定方法
        MessageHandler handler = ShowMessage;
        ProcessMessage(handler);
    }

    // 符合委托签名的方法
    static void ShowMessage(string msg)
    {
        Console.WriteLine($"收到消息: {msg}");
    }
}

这个例子展示了委托的核心特点:类型安全的方法引用。但实际开发中,我们更多使用事件来避免直接暴露委托。

二、事件的内存陷阱:订阅者的生命周期

事件虽然方便,但如果不注意就会导致内存泄漏。最常见的问题是:当事件发布者比订阅者生命周期更长时,订阅者无法被垃圾回收。看这个典型场景:

public class EventPublisher
{
    public event EventHandler SomethingHappened;

    public void DoSomething()
    {
        SomethingHappened?.Invoke(this, EventArgs.Empty);
    }
}

public class EventSubscriber
{
    public EventSubscriber(EventPublisher publisher)
    {
        // 订阅事件(潜在的内存泄漏点)
        publisher.SomethingHappened += OnSomethingHappened;
    }

    private void OnSomethingHappened(object sender, EventArgs e)
    {
        Console.WriteLine("事件被触发");
    }
}

class Program
{
    static void Main()
    {
        var publisher = new EventPublisher();
        var subscriber = new EventSubscriber(publisher);
        
        // 即使subscriber不再使用,由于publisher仍持有引用,它不会被GC回收
        publisher.DoSomething();
    }
}

这里的关键问题是:事件的+=操作会创建一个强引用。除非显式使用-=,否则订阅者会一直被发布者"拽着"不放。

三、解决方案:四种正确使用模式

1. 显式取消订阅模式

public class SafeSubscriber : IDisposable
{
    private EventPublisher _publisher;
    
    public SafeSubscriber(EventPublisher publisher)
    {
        _publisher = publisher;
        _publisher.SomethingHappened += OnSomethingHappened;
    }
    
    private void OnSomethingHappened(object sender, EventArgs e) { /*...*/ }
    
    public void Dispose()
    {
        // 关键:在不再需要时取消订阅
        _publisher.SomethingHappened -= OnSomethingHappened;
    }
}

2. 弱事件模式(使用WeakReference)

public class WeakEventManager
{
    private WeakReference<EventHandler> _weakHandler;
    
    public void AddHandler(EventHandler handler)
    {
        _weakHandler = new WeakReference<EventHandler>(handler);
    }
    
    public void RaiseEvent()
    {
        if (_weakHandler.TryGetTarget(out var handler))
        {
            handler(this, EventArgs.Empty);
        }
    }
}

3. 使用框架提供的WeakEventManager

.NET提供了现成的解决方案:

public class MyEventSource
{
    public static readonly WeakEventManager WeakEventManager = new();
    
    public event EventHandler MyEvent
    {
        add => WeakEventManager.AddHandler(value);
        remove => WeakEventManager.RemoveHandler(value);
    }
}

4. 基于接口的订阅模式

public interface IEventSubscriber
{
    void HandleEvent();
}

public class EventBus
{
    private readonly List<WeakReference<IEventSubscriber>> _subscribers = new();
    
    public void Subscribe(IEventSubscriber subscriber)
    {
        _subscribers.Add(new WeakReference<IEventSubscriber>(subscriber));
    }
    
    public void Publish()
    {
        foreach (var weakRef in _subscribers.ToList())
        {
            if (weakRef.TryGetTarget(out var subscriber))
            {
                subscriber.HandleEvent();
            }
            else
            {
                _subscribers.Remove(weakRef); // 自动清理
            }
        }
    }
}

四、实战建议与最佳实践

  1. 对象生命周期管理

    • 确保订阅者的生命周期不超过发布者
    • 对于长期存在的发布者,优先考虑弱引用模式
  2. 清理策略

    // 在适当的位置(如Dispose、析构函数中)取消订阅
    public void Dispose()
    {
        _publisher.Event -= HandlerMethod;
    }
    
  3. 性能考量

    • 弱引用模式会增加少量性能开销
    • 高频事件建议使用强引用+显式管理
  4. 调试技巧

    // 检查事件订阅者数量
    var count = myEvent?.GetInvocationList()?.Length ?? 0;
    Console.WriteLine($"当前事件订阅数: {count}");
    
  5. 框架选择

    • WPF应用优先使用WeakEventManager
    • 纯.NET环境可以考虑自定义弱引用实现
    • 跨组件通信推荐使用事件聚合器模式

记住,正确管理事件订阅关系不仅能避免内存泄漏,还能使程序结构更清晰。下次当你使用+=操作符时,不妨先想想:"这个订阅关系将来要怎么解除?"