基础教程中我们已经掌握了委托的声明、实例化和基本使用,以及事件的发布/订阅模式。但在真实的框架和大型应用中,委托与事件涉及更多高级话题:协变/逆变、多播委托的异常处理、事件访问器、弱事件模式、以及线程安全的事件触发。本文将逐一深入剖析。
委托的协变与逆变
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class Animal { } class Dog : Animal { }
public delegate Animal AnimalFactory(); public static Dog CreateDog() => new Dog();
AnimalFactory factory = CreateDog;
public delegate void AnimalHandler(Dog dog); public static void HandleAnimal(Animal animal) { }
AnimalHandler handler = HandleAnimal;
|
在 .NET Framework 4.0 之前,泛型委托不支持这种类型族关系。引入 in/out 关键字后,Func<out TResult> 和 Action<in T> 让委托类型在继承体系中更加灵活。
多播委托的异常处理
多播委托按顺序执行调用列表中的每个方法,但一个问题在于:如果某个调用抛出异常,后续的调用将不会执行。
1 2 3 4 5 6 7 8 9 10 11 12
| Action pipeline = () => Console.WriteLine("Step 1"); pipeline += () => throw new InvalidOperationException("Step 2 failed"); pipeline += () => Console.WriteLine("Step 3");
try { pipeline(); } catch (InvalidOperationException ex) { Console.WriteLine($"异常: {ex.Message}"); }
|
解决方案 —— 手动遍历调用列表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| Action safePipeline = () => Console.WriteLine("Step 1"); safePipeline += () => throw new InvalidOperationException("Step 2 failed"); safePipeline += () => Console.WriteLine("Step 3");
foreach (Action step in safePipeline.GetInvocationList()) { try { step(); } catch (Exception ex) { Console.WriteLine($"步骤异常: {ex.Message}"); } }
|
事件访问器(add / remove)
事件在默认情况下生成一个私有的多播委托字段。但你可以自定义事件访问器来控制存储行为。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| private EventHandler? _handler;
public event EventHandler CustomEvent { add { Console.WriteLine($"订阅者添加: {value?.Method?.Name}"); _handler += value; } remove { Console.WriteLine($"订阅者移除: {value?.Method?.Name}"); _handler -= value; } }
|
典型应用场景:
- 线程安全的锁保护事件添加/移除
- 事件订阅日志记录
- 只有特定类型才允许订阅事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| private EventHandler? _safeHandler; private readonly object _lock = new();
public event EventHandler SafeEvent { add { lock (_lock) { _safeHandler += value; } } remove { lock (_lock) { _safeHandler -= value; } } }
protected virtual void OnSafeEvent(EventArgs e) { EventHandler? handler; lock (_lock) { handler = _safeHandler; } handler?.Invoke(this, e); }
|
弱事件模式
传统事件绑定会导致发布者持有订阅者对象的强引用,当订阅者应当被回收时,由于发布者仍引用其事件处理器,GC 无法回收,造成内存泄漏。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class Publisher { public event EventHandler? DataChanged; public void Raise() => DataChanged?.Invoke(this, EventArgs.Empty); }
class Subscriber { public void OnDataChanged(object? sender, EventArgs e) { } }
var publisher = new Publisher(); var subscriber = new Subscriber(); publisher.DataChanged += subscriber.OnDataChanged;
|
WeakEventManager 解决了该问题(WPF 中常见):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| using System.Windows;
public class WeakPublisher { private readonly WeakEventManager _manager = new();
public event EventHandler DataChanged { add => _manager.AddEventHandler(this, value!); remove => _manager.RemoveEventHandler(this, value!); }
public void Raise() { _manager.HandleEvent(this, EventArgs.Empty, nameof(DataChanged)); } }
|
对于非 WPF 项目,也可以手动使用 WeakReference<T> 实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class WeakEventHandler<TTarget> where TTarget : class { private readonly WeakReference<TTarget> _target; private readonly Action<TTarget, object?, EventArgs> _action;
public WeakEventHandler(TTarget target, Action<TTarget, object?, EventArgs> action) { _target = new WeakReference<TTarget>(target); _action = action; }
public void Invoke(object? sender, EventArgs e) { if (_target.TryGetTarget(out var target)) { _action(target, sender, e); } } }
|
线程安全的事件触发
1 2 3 4 5 6 7 8 9 10 11 12
| public class ThreadSafePublisher { public event EventHandler<DataEventArgs>? DataReceived;
protected virtual void OnDataReceived(DataEventArgs e) { EventHandler<DataEventArgs>? handler = Interlocked.CompareExchange( ref DataReceived, null, null); handler?.Invoke(this, e); } }
|
或者更简洁地使用 C# 的模式匹配:
1 2 3 4
| protected virtual void OnDataReceived(DataEventArgs e) { Volatile.Read(ref DataReceived)?.Invoke(this, e); }
|
现代 C# 的事件替代方案
随着 C# 语言发展,部分场景开始倾向于不使用原始事件:
| 替代方案 |
适用场景 |
IObservable<T> / IObserver<T> |
基于 Rx.NET 的响应式编程 |
IProgress<T> |
异步操作进度报告 |
TaskCompletionSource<T> |
一次性异步信号通知 |
Channel<T> |
生产者-消费者模式 |
INotifyPropertyChanged |
MVVM 数据绑定 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class ProgressReporter { public async Task DoWork(IProgress<int> progress) { for (int i = 0; i <= 100; i += 10) { progress.Report(i); await Task.Delay(200); } } }
var reporter = new ProgressReporter(); var progress = new Progress<int>(p => Console.WriteLine($"进度: {p}%")); await reporter.DoWork(progress);
|
总结:高级委托与事件技巧是编写健壮、可维护的 C# 框架的基础。多播委托的异常隔离、自定义事件访问器的灵活控制、弱事件模式的内存安全、以及线程安全的触发模式,这些都是在大型项目中必须掌握的技能。同时应当关注现代替代方案,根据场景选择最合适的技术。