1. 理解C#事件与委托的核心概念
第一次接触C#事件和委托时,我完全被这两个概念搞晕了。记得当时在做一个WinForms项目,需要处理按钮点击,看到那个+=符号和一堆EventHandler的时候,简直一头雾水。后来才发现,委托和事件其实是C#中最强大也最容易被误解的特性之一。
委托(delegate)本质上是一种类型安全的函数指针。它定义了方法的签名,可以引用任何具有兼容签名的方法。举个例子,假设我们有一个计算器程序,可以这样定义一个委托:
csharp复制public delegate int CalculateDelegate(int x, int y);
这个委托可以指向任何接受两个int参数并返回int的方法。委托的强大之处在于它允许我们将方法作为参数传递,实现回调机制。
事件(event)则是建立在委托基础上的更高级抽象。事件是一种特殊的委托,遵循特定的模式,主要用于实现发布-订阅模型。在WinForms中,按钮的Click事件就是最典型的例子:
csharp复制button1.Click += Button1_Click;
private void Button1_Click(object sender, EventArgs e)
{
// 处理点击事件
}
关键区别:委托是类型,事件是委托的实例加上一层封装。事件提供了更好的封装性,外部只能通过+=和-=来订阅和取消订阅,而不能直接调用或赋值。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 委托的底层实现与高级用法
2.1 委托的编译后真相
很多人不知道的是,C#编译器会把委托编译成一个继承自System.MulticastDelegate的类。这个类包含三个关键方法:
- Invoke:同步调用委托链中的所有方法
- BeginInvoke:异步开始调用
- EndInvoke:获取异步调用结果
当我们这样定义一个委托时:
csharp复制public delegate void MyDelegate(string message);
编译器实际上会生成一个完整的类:
csharp复制public class MyDelegate : System.MulticastDelegate
{
public MyDelegate(object @object, IntPtr method);
public virtual void Invoke(string message);
public virtual IAsyncResult BeginInvoke(string message, AsyncCallback callback, object @object);
public virtual void EndInvoke(IAsyncResult result);
}
2.2 多播委托与调用链
委托最强大的特性之一是支持多播(multicast),即一个委托实例可以引用多个方法。当调用这个委托时,所有方法会按添加顺序依次执行:
csharp复制MyDelegate del = Method1;
del += Method2;
del += Method3;
del("Hello"); // 依次调用Method1、Method2、Method3
但这里有个坑需要注意:如果委托有返回值,只有最后一个方法的返回值会被返回,前面的返回值会被丢弃。同样,如果委托有out参数,也只有最后一个方法的out参数值会生效。
2.3 泛型委托与内置委托类型
现代C#提供了几种内置的泛型委托类型,可以满足大多数场景:
- Action:无返回值的方法
- Func:有返回值的方法
- Predicate:返回bool的谓词方法
例如:
csharp复制Action<string> log = message => Console.WriteLine(message);
Func<int, int, int> add = (x, y) => x + y;
Predicate<string> isLong = s => s.Length > 10;
使用这些内置委托可以减少自定义委托类型的定义,使代码更简洁。
3. 事件机制的深入解析
3.1 标准事件模式
C#中的事件遵循特定的模式,通常包含:
- 一个继承自EventArgs的类,用于传递事件数据
- 一个委托类型,通常命名为"EventHandler"
- 一个事件成员
- 一个受保护的虚方法,用于引发事件
标准实现如下:
csharp复制public class PriceChangedEventArgs : EventArgs
{
public decimal OldPrice { get; }
public decimal NewPrice { get; }
public PriceChangedEventArgs(decimal oldPrice, decimal newPrice)
{
OldPrice = oldPrice;
NewPrice = newPrice;
}
}
public class Stock
{
public event EventHandler<PriceChangedEventArgs> PriceChanged;
protected virtual void OnPriceChanged(PriceChangedEventArgs e)
{
PriceChanged?.Invoke(this, e);
}
private decimal price;
public decimal Price
{
get => price;
set
{
if (price == value) return;
decimal oldPrice = price;
price = value;
OnPriceChanged(new PriceChangedEventArgs(oldPrice, price));
}
}
}
3.2 事件访问器
事件实际上是一对add和remove方法的语法糖。我们可以自定义这些访问器来实现更复杂的行为:
csharp复制private EventHandler<PriceChangedEventArgs> priceChanged;
public event EventHandler<PriceChangedEventArgs> PriceChanged
{
add
{
priceChanged += value;
Console.WriteLine($"Added handler: {value.Method.Name}");
}
remove
{
priceChanged -= value;
Console.WriteLine($"Removed handler: {value.Method.Name}");
}
}
这在需要跟踪事件订阅或实现弱引用事件模式时非常有用。
3.3 弱事件模式
事件的一个常见问题是可能导致内存泄漏,因为事件源会持有对订阅者的强引用。为了解决这个问题,可以使用弱事件模式:
csharp复制public class WeakEventManager
{
private readonly List<WeakReference<EventHandler<EventArgs>>> handlers = new();
public void AddHandler(EventHandler<EventArgs> handler)
{
handlers.Add(new WeakReference<EventHandler<EventArgs>>(handler));
}
public void RemoveHandler(EventHandler<EventArgs> handler)
{
// 实现移除逻辑
}
public void RaiseEvent(object sender, EventArgs args)
{
foreach (var weakRef in handlers.ToArray())
{
if (weakRef.TryGetTarget(out var handler))
{
handler(sender, args);
}
else
{
handlers.Remove(weakRef);
}
}
}
}
.NET也提供了WeakEventManager类来简化这一模式的实现。
4. 常见问题与最佳实践
4.1 事件与委托的线程安全问题
事件引发时的一个常见错误是忽略了线程安全。以下代码存在竞态条件:
csharp复制if (PriceChanged != null)
PriceChanged(this, e);
在检查null之后,另一个线程可能取消订阅,导致PriceChanged变为null。正确的做法是:
csharp复制PriceChanged?.Invoke(this, e);
C# 6.0引入的空条件运算符?.会原子性地完成null检查和调用。
4.2 内存泄漏与事件订阅
事件订阅可能导致内存泄漏,因为事件源会持有对订阅者的引用。典型场景:
csharp复制public class Publisher
{
public event EventHandler SomethingHappened;
}
public class Subscriber
{
public Subscriber(Publisher pub)
{
pub.SomethingHappened += HandleSomething;
}
private void HandleSomething(object sender, EventArgs e) { }
}
如果Subscriber实例不再需要但没有取消订阅,Publisher会阻止Subscriber被垃圾回收。解决方法:
- 实现IDisposable来取消订阅
- 使用弱事件模式
- 确保在适当的时候取消订阅
4.3 性能考量
委托调用比直接方法调用稍慢,因为涉及额外的间接层。在性能关键路径上,可以考虑:
- 缓存委托实例而不是每次都创建新的
- 对于简单场景,使用接口而不是委托
- 避免在紧密循环中创建和销毁大量委托
4.4 事件与异步编程
在异步环境中使用事件需要注意:
csharp复制public event EventHandler<MyEventArgs> MyEvent;
// 不安全的异步引发事件
public async Task RaiseEventAsync()
{
await Task.Delay(100);
MyEvent?.Invoke(this, new MyEventArgs()); // 可能在错误的上下文中执行
}
// 安全的做法
public async Task RaiseEventAsync()
{
await Task.Delay(100);
var handlers = MyEvent?.GetInvocationList();
if (handlers != null)
{
foreach (var handler in handlers)
{
if (handler is EventHandler<MyEventArgs> h)
{
await Task.Run(() => h(this, new MyEventArgs()));
}
}
}
}
5. 实际应用案例
5.1 实现一个简单的消息总线
利用事件和委托可以轻松实现进程内的消息总线:
csharp复制public class MessageBus
{
private static readonly Lazy<MessageBus> instance = new(() => new MessageBus());
public static MessageBus Instance => instance.Value;
private readonly Dictionary<Type, Delegate> handlers = new();
public void Subscribe<T>(Action<T> handler)
{
if (handlers.TryGetValue(typeof(T), out var existing))
{
handlers[typeof(T)] = Delegate.Combine(existing, handler);
}
else
{
handlers[typeof(T)] = handler;
}
}
public void Publish<T>(T message)
{
if (handlers.TryGetValue(typeof(T), out var handler))
{
((Action<T>)handler)(message);
}
}
}
// 使用示例
MessageBus.Instance.Subscribe<string>(msg => Console.WriteLine($"收到消息: {msg}"));
MessageBus.Instance.Publish("Hello World");
5.2 实现可观察的集合
结合INotifyPropertyChanged和INotifyCollectionChanged,我们可以创建完全可观察的数据结构:
csharp复制public class ObservableCollection<T> : INotifyCollectionChanged, INotifyPropertyChanged
{
private readonly List<T> items = new();
public event NotifyCollectionChangedEventHandler CollectionChanged;
public event PropertyChangedEventHandler PropertyChanged;
public void Add(T item)
{
items.Add(item);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Add, item, items.Count - 1));
OnPropertyChanged(nameof(Count));
}
protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
CollectionChanged?.Invoke(this, e);
}
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public int Count => items.Count;
}
5.3 实现命令模式
在MVVM模式中,命令通常用委托实现:
csharp复制public class DelegateCommand : ICommand
{
private readonly Action<object> execute;
private readonly Func<object, bool> canExecute;
public event EventHandler CanExecuteChanged;
public DelegateCommand(Action<object> execute, Func<object, bool> canExecute = null)
{
this.execute = execute ?? throw new ArgumentNullException(nameof(execute));
this.canExecute = canExecute;
}
public bool CanExecute(object parameter) => canExecute?.Invoke(parameter) ?? true;
public void Execute(object parameter) => execute(parameter);
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
// 使用示例
public class ViewModel
{
public ICommand SaveCommand { get; }
public ViewModel()
{
SaveCommand = new DelegateCommand(ExecuteSave, CanExecuteSave);
}
private void ExecuteSave(object parameter) { /* 保存逻辑 */ }
private bool CanExecuteSave(object parameter) { return true; }
}
6. 高级主题与未来方向
6.1 源生成器与事件
C# 9.0引入的源生成器可以自动生成事件样板代码。例如,我们可以创建一个源生成器来自动实现INotifyPropertyChanged:
csharp复制[Generator]
public class NotifyPropertyChangedGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
}
public void Execute(GeneratorExecutionContext context)
{
if (context.SyntaxContextReceiver is not SyntaxReceiver receiver)
return;
foreach (var classDecl in receiver.CandidateClasses)
{
var source = GenerateClassSource(classDecl);
context.AddSource($"{classDecl.Identifier.Text}_generated.cs", source);
}
}
private string GenerateClassSource(ClassDeclarationSyntax classDecl)
{
// 生成实现INotifyPropertyChanged的完整类代码
// 包括所有属性的通知逻辑
}
}
6.2 记录(Record)与事件
C# 9.0的记录类型(record)与事件结合使用时需要注意,因为记录默认是不可变的:
csharp复制public record Order(int Id, string ProductName)
{
public event EventHandler Shipped;
public void Ship()
{
// 发货逻辑
Shipped?.Invoke(this, EventArgs.Empty);
}
}
由于记录在with表达式中会创建新实例,事件订阅不会被复制:
csharp复制var order1 = new Order(1, "Book");
order1.Shipped += (s, e) => Console.WriteLine("Shipped!");
var order2 = order1 with { ProductName = "Updated Book" };
order2.Ship(); // 不会触发事件,因为order2有新的Shipped委托实例
6.3 异步事件模式
传统的同步事件模式在处理异步操作时不太友好。一种改进模式是使用异步事件处理程序:
csharp复制public delegate Task AsyncEventHandler<TEventArgs>(object sender, TEventArgs e);
public class AsyncEvent<TEventArgs>
{
private readonly List<AsyncEventHandler<TEventArgs>> handlers = new();
public async Task InvokeAsync(object sender, TEventArgs e)
{
foreach (var handler in handlers.ToArray())
{
await handler(sender, e).ConfigureAwait(false);
}
}
public void AddHandler(AsyncEventHandler<TEventArgs> handler)
{
handlers.Add(handler);
}
public void RemoveHandler(AsyncEventHandler<TEventArgs> handler)
{
handlers.Remove(handler);
}
}
这种模式允许事件处理程序执行异步操作,同时保持顺序执行。
