C# 高级教程 - 04 高级委托与事件

基础教程中我们已经掌握了委托的声明、实例化和基本使用,以及事件的发布/订阅模式。但在真实的框架和大型应用中,委托与事件涉及更多高级话题:协变/逆变、多播委托的异常处理、事件访问器、弱事件模式、以及线程安全的事件触发。本文将逐一深入剖析。

委托的协变与逆变

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; // 协变:Dog(子类)→ Animal(父类)

// 逆变:参数可以是基类类型
public delegate void AnimalHandler(Dog dog);
public static void HandleAnimal(Animal animal) { }

AnimalHandler handler = HandleAnimal; // 逆变:Animal(父类)→ Dog(子类)

在 .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}"); // Step 3 从未执行
}

解决方案 —— 手动遍历调用列表

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}");
}
}
// 输出:
// Step 1
// 步骤异常: Step 2 failed
// Step 3

事件访问器(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) { }
}

// 泄漏:publisher 强引用 subscriber
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)
{
// 标准线程安全模式:复制引用再调用,防止订阅者在 null 检查和调用之间被移除
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# 框架的基础。多播委托的异常隔离、自定义事件访问器的灵活控制、弱事件模式的内存安全、以及线程安全的触发模式,这些都是在大型项目中必须掌握的技能。同时应当关注现代替代方案,根据场景选择最合适的技术。

ByteFisher
分享编程技术 · 记录钓鱼乐趣
扫码关注
▸ 扫码关注 ◂
分享: