C# 委托与事件
2023 年 11 月 29 日 · 2 分钟读完
刚学 C# 时,委托和事件经常傻傻分不清。它们功能相似,用途却不一样,分开讲完再比较。
委托(Delegate)
委托是一种类型,它的值是对一个或多个方法的引用,类似 C 的函数指针。有了它,方法就能当参数传递、当变量存起来。
基本用法
三步:声明委托类型、定义委托变量、绑定签名一致的方法。
delegate bool Compare(int a, int b); // 1. 声明类型:两个 int 进,bool 出
static Compare w; // 2. 定义变量
static bool Greater(int a, int b) => a > b;
static void Main(string[] args)
{
w += Greater; // 3. 绑定
Console.WriteLine(w(1, 2)); // 调用 w 就是调用 Greater,输出 False
}
无参数、无返回值的委托写法完全一样,只是签名换成 delegate void Write();。

多播委托
一个委托变量可以用 += 绑定多个方法,调用时按绑定顺序依次执行。带返回值时有个坑:
static bool Greater(int a, int b) => a > b; // 返回 false
static bool Less(int a, int b) => a < b; // 返回 true
w += Greater;
w += Less;
Console.WriteLine(w(1, 2)); // True

两个方法都执行了,但只拿到最后一个方法的返回值,前面的结果都丢了。
处理每一个返回值
想要所有结果,就用 GetInvocationList() 自己一个个调:
static bool AllTrue(int a, int b)
{
bool ret = true;
if (w != null)
{
foreach (Compare item in w.GetInvocationList())
{
ret &= item(a, b); // 有一个 false 就是 false
}
}
return ret;
}
Console.WriteLine(AllTrue(1, 2)); // False

事件(Event)
事件是对委托的封装,用来实现观察者模式(发布-订阅)。它对外只开放 += 和 -=:
- 外部只能订阅、取消订阅;
- 外部不能直接调用,也不能
= null把别人的订阅清掉; - 只有声明事件的类自己能触发。
基本用法
public delegate bool Compare(int a, int b);
public class Sensor
{
public event Compare WriteEvent;
public void Trigger(int a, int b)
{
WriteEvent?.Invoke(a, b); // 没人订阅时为 null,所以用 ?.
}
}
static bool Print(int a, int b)
{
Console.WriteLine(a + b);
return a > b;
}
var t = new Sensor();
t.WriteEvent += Print;
t.WriteEvent += Print; // 同一个方法订阅了两次
t.Trigger(1, 2); // 打印两次 3
// t.WriteEvent(1, 2); // 编译错误:类外部不能触发事件

事件不会判断重复订阅,订阅几次就执行几次。
防止重复订阅
自己实现 add、remove 访问器:
public class Sensor
{
private Compare _handlers;
public event Compare WriteEvent
{
add
{
if (_handlers != null && _handlers.GetInvocationList().Contains(value))
return;
_handlers += value;
}
remove
{
_handlers -= value;
}
}
public void Trigger(int a, int b)
{
// 自定义访问器以后,事件名不能再直接调用,要调用背后的委托字段
_handlers?.Invoke(a, b);
}
}

区别
- 委托是类型,可以把方法当参数传递、存储,拿到委托变量的人都能直接调用它。
- 事件是委托的封装(类的成员),提供更安全的订阅机制,只有持有类能触发。
延伸
- 大多数时候不用自己声明委托类型:
Action系列表示无返回值(Action<int, int>),Func系列最后一个类型参数是返回值(Func<int, int, bool>就等于上面的Compare),Predicate<T>返回 bool。配合 lambda:Func<int, int, bool> w = (a, b) => a > b;。 - 事件的标准写法:
public event EventHandler<DataEventArgs> DataReceived;,参数是(object sender, DataEventArgs e),DataEventArgs继承EventArgs。WinForm 的所有控件事件都是这个形状,团队里统一用它最省心。 - 忘记
-=会内存泄漏:订阅者的生命周期短(比如一个弹出窗体)、发布者的生命周期长(比如全局的 PLC 通讯对象)时,窗体关闭后仍被事件引用,回收不掉,事件还会继续往已关闭的窗体里调。关闭时一定取消订阅,见 什么对象会被 .NET 垃圾回收。 - lambda 订阅取消不掉:
e += (s, a) => Foo();之后再写一遍同样的 lambda 去-=是无效的,因为是两个不同的委托实例。需要取消的订阅,要先把 lambda 存到变量里。 - 事件在哪个线程触发,处理方法就在哪个线程执行:通讯线程里触发的事件,处理方法里直接改界面控件会报跨线程异常,要用
Invoke,见 C# 扩展方法 里的InvokeIfRequired。
原文首发于 blog.csdn.net