当我们希望“某事发生时,自动通知别人”,或者把“能做的事”当作一等公民四处传递(函数当参数/返回值)时,C# 给我们的工具就是“委托(delegate)”与“事件(event)”。
把委托想成“类型安全的函数名片”,把事件想成“公告栏”:发布者贴出通知,订阅者各自处理。——这就是松耦合的开始。

委托其实就像是C#世界里的“函数名片”——我们可以把它当作一种特殊的类型,用来存放某种特定签名的函数。这 样一来,我们不仅能把函数像普通变量一样传递给别的方法,还能把它们作为返回值带回去。
比如说,如果我们有一个方法需要“等别人来决定怎么做”,那就可以让它接收一个委托参数,把“做法”交给外部决定。 委托让函数变得像积木一样灵活,可以随意拼接和传递,这也是C#实现回调、事件通知等机制的基础。
|using System; // 自定义委托:签名为 (int,int) -> int public delegate int BinaryOp(int a, int b); class Program { static int Add(int x, int y) => x + y; // 符合签名 static int Mul(int x, int y) => x * y; // 符合签名 static void Main() { BinaryOp op = Add; // 函数名片:装进委托 Console.WriteLine(op(2, 3)); // 5 op = Mul; // 可互换 Console.WriteLine(op(2, 3)); // 6 } }
我们可以把委托想象成一张写明了“你需要什么材料、会得到什么结果”的说明书。只要有符合这个说明书要求的做法(也就是函数),都可以随时换上去。 比如说,今天我们让委托代表“两个整数相加”,明天又可以让它代表“两个整数相乘”,只要参数和返回值的类型对得上,委托就能灵活切换“具体怎么做”。 这样,委托就像一个万能插座,插头(函数)怎么换都行,只要形状(签名)对得上。
在C#的世界里,我们经常会遇到这样一种需求:我想把“做某件事的方法”当作参数传给别人,或者让别人把“判断某个条件的方法”交给我。为此,C#贴心地为我们准备了三位“万能小助手”——Action、Func 和 Predicate。
我们可以把Action想象成“只做事不回报”的小工人。比如说,你让他打印一句话、写个文件、发个通知,他都能干,但干完就拍拍屁股走人,不会给你任何结果。Action的签名就是“有参数没返回值”。 Func则像是“既能干活还能给你结果”的多面手。你让他算个数、拼个字符串、查个数据库,他都能干,而且干完会把结果交给你。Func的签名是“有参数有返回值”,最后一个类型参数就是返回值类型。 Predicate则是“专门用来判断对错”的小裁判。你给他一个东西,他会告诉你“是”还是“否”,也就是返回一个bool。Predicate的签名就是“有一个参数,返回bool”。
这三位小助手让我们写代码时变得非常灵活。比如说,我们想遍历一堆数字,把每个数字都打印出来,就可以用Action;想把两个数加起来,就用Func;想判断一个数是不是偶数,就用Predicate。这样,代码就像搭积木一样,想怎么拼就怎么拼。
|using System; Action<string> print = s => Console.WriteLine(s); // (T) -> void Func<int, int, int> add = (a,b) => a + b; // (T1,T2) -> TResult Predicate<int> isEven = n =>
|using System; Action chain = null; chain += () => Console.WriteLine("A"); chain += () => Console.WriteLine("B"); chain(); // A 换行 B Func<int> f = () => 1; f +=
多播委托其实就像是一个广播站,订阅了它的每个方法都会被依次叫到名字、轮流执行一遍。比如说,我们给多播委托挂了三个方法A、B、C,那么每次调用这个委托时,A、B、C都会被按顺序执行。
如果这个多播委托有返回值(比如Func类型),那它只会把最后一个方法的返回值作为最终结果。前面的方法虽然也执行了,但它们的返回值会被“悄悄丢掉”。
在C#里,事件其实就是在委托的基础上又加了一道“门禁”。我们可以把委托想象成一把万能钥匙,谁拿到都能随意开门(也就是随意赋值、触发); 而事件就像是房东在门外装了个门禁系统,外人只能登记进出(只能+=或-=订阅/取消订阅),但不能自己开门进去(不能直接赋值或触发)。 这样,只有房东(也就是事件的发布者)才能决定什么时候开门(触发事件),而订阅者只能在门外等着通知,保证了内部的安全和封装。
|using System; class Button { public event Action? Clicked; // 只能 += / -= public void Click() => Clicked?.Invoke(); // 仅发布者内部可触发 } class Program { static void Main() { var btn = new Button
其实在C#的世界里,还有一套“标准流程”来定义和触发事件,这就是 EventHandler 和 EventArgs 的组合。
想象一下,我们有一个温度计(Thermometer)放在房间里。每当温度发生变化时,温度计就会大声喊:“温度变啦!”——这就是事件。而我们(比如空调、加湿器)可以订阅这个通知,随时准备响应。 C#里,EventHandler 就像是一个标准的广播喇叭,规定了广播内容的格式:第一个参数是“谁”发的消息(sender),第二个参数是消息的详细内容(EventArgs)。EventArgs 是个基类,我们可以继承它,装上自己想传递的各种信息。
比如说,我们想让温度计在温度变化时,不仅告诉大家“变了”,还要说清楚“从多少变到多少”。这时,我们就可以自定义一个继承自 EventArgs 的类,把旧温度和新温度都装进去。
下面是一个示例:
|using System; // 自定义事件参数:携带更多上下文 public class TemperatureChangedEventArgs : EventArgs { public double OldValue { get; } public double NewValue { get; } public TemperatureChangedEventArgs(double oldV, double newV){ OldValue = oldV; NewValue = newV; } } public class
在C#的标准事件模式中,我们会发现事件的签名总是有一种“约定俗成”的统一格式。每当我们定义一个事件时,这个事件处理方法(也就是委托)的第一个参数总是代表“谁”触发了这个事件,我们通常叫它 sender,它其实就是事件的源头。
第二个参数则是用来装载事件相关的详细信息,这个参数的类型一般是 EventArgs 或者继承自 EventArgs 的自定义类。这
样设计的好处是,无论我们监听什么事件,都能通过 sender 知道消息从哪儿来,通过 EventArgs 了解事件发生时的具体细节。
比如温度计的例子里,sender 就是温度计本身,而 TemperatureChangedEventArgs 里则记录了温度变化的前后数值
在C#中,事件不仅可以像我们前面那样直接声明,还可以通过“自定义事件访问器”来精细控制事件的订阅和退订过程。
我们可以把事件想象成一个广播站,大家都可以来订阅消息,但有时候我们希望在有人订阅或取消订阅时,做一些额外的事情,比如记录日志、统计当前有多少人关注,甚至做权限校验。
这时候,C#允许我们为事件专门写上add和remove访问器,就像给门口加了保安一样,每次有人进出都能被记录下来。
假设有一辆公交车(Bus),每次到站都会广播“我到了!”。有些乘客(订阅者)想知道公交车什么时候到站,于是他们订阅了这个消息。 公交车司机希望每次有人订阅或退订时都能在控制台上看到提示,这样他就知道有多少人关心他的到站信息。
下面我们用代码来实现这个场景:
|public class Bus { private EventHandler? _arrived; public event EventHandler Arrived { add { _arrived += value; Console.WriteLine("订阅 +1"); } remove { _arrived -= value; Console.WriteLine("退订 -1"); } } public void OnArrived() => _arrived?.
当需要统计订阅数、做权限控制或跨进程转发时,可使用自定义访问器。
事件 vs 接口 vs 回调函数:如何取舍
事件传递的是“发生了某事”的通知,而非“我要一个答案”。如果你需要收集返回值,请选择委托(返回 Task/结果)或定义接口。
事件签名传统上是 void。若订阅者需要异步,可以:
EventHandler<T>,订阅者内部自行 async void(UI)或 Task.Run,但不可聚合等待;Func<object?,TEventArgs,Task>,触发时 await Task.WhenAll(...)。|using System; using System.Linq; using System.Threading.Tasks; public class AsyncBus<T> { private event Func<object?, T, Task>? _handlers; // 非传统 EventHandler,但更利于异步聚合 public void Subscribe(Func<
4. 自定义委托与切换实现练习
实现自定义委托 ToText,并演示如何切换不同的实现:
public delegate string ToText(int x);x 是偶数返回 "Even:x",否则返回 "Odd:x"x.ToString()Main 方法中测试两种实现|using System; public delegate string ToText(int x); class Program { static void Main() { // 实现1:偶数返回 "Even:x",奇数返回 "Odd:x" ToText toText1 = x => x % 2 == 0 ? $"Even:{x}" : $"Odd:{
5. Action 链式调用顺序练习
分析以下代码,理解多播委托的执行顺序:
Action a = null; 创建一个空的 Action 委托a += ()=>Console.Write("A"); 添加方法Aa+=()=>Console.Write("B"); 添加方法Ba(); 调用委托|using System; class Program { static void Main() { Action a = null; a += () => Console.Write("A"); a += () => Console.Write("B"); a(); // 输出: AB Console.WriteLine
6. 闭包修复练习
修正以下代码,解决闭包变量捕获问题:
ii,执行时 i 的值已经是循环结束后的值(3)012 而不是 333|using System; using System.Collections.Generic; class Program { static void Main() { // 错误示例:所有 lambda 捕获同一个 i var list1 = new List<Action>(); for (int i = 0; i < 3; i++)
7. EventHandler 标准签名练习
编写一个使用 EventHandler<string> 的事件,触发时输出 sender 的类型和字符串消息:
EventHandler<string> 类型的事件(object? sender, string e)Main 方法中订阅事件并触发,验证输出|using System; class Publisher { public event EventHandler<string>? MessageReceived; public void Publish(string message) { MessageReceived?.Invoke(this, message); } } class Program { static void Main
8. 多播返回值练习
分析多播委托的返回值行为:
Func<int> f = ()=>1; 创建返回 1 的委托f+=()=>2; 添加返回 2 的委托var r = f(); 调用委托r 的值,并说明如何获取所有返回值|using System; class Program { static void Main() { // 多播委托的返回值 Func<int> f = () => 1; f += () => 2; f += () => 3; // 只返回最后一个方法的返回值 int r =
9. 取消订阅避免泄漏练习
解释为什么长生命周期发布者持有短生命周期订阅者会导致内存泄漏,以及如何避免:
-=)|using System; class Publisher { public event EventHandler<string>? MessageReceived; public void Publish(string msg) => MessageReceived?.Invoke(this, msg); } class Subscriber { private readonly string _name;
|实现1: Even:2 Odd:3 实现2: 2 3 当前使用实现1: Even:4 切换到实现2: 4
说明:
|AB ABC BC
说明:
+= 用于添加方法到委托链-= 用于从委托链中移除方法null,使用 += 会自动创建新的委托实例|错误示例: 333 正确示例: 012
说明:
i 在循环外只有一个实例,所有 lambda 都捕获同一个 icopy,每次循环迭代都有独立的 copycopy,而不是共享的 i|Sender类型: Publisher 消息内容: Hello, World!
说明:
EventHandler<string> 是标准的事件处理委托类型sender 是触发事件的对象(通常是 this)string 类型)?.Invoke() 安全触发事件(如果为 null 则不触发)|直接调用返回值: 3 所有返回值: 1 2 3
说明:
f() 只返回最后一个方法的返回值(3)GetInvocationList() 获取所有订阅的方法,然后逐个调用GetInvocationList()|临时订阅者收到: 消息1 正确做法:取消订阅 正确订阅者收到: 消息2
说明:
-= 取消订阅IDisposable 接口,在 Dispose 中取消订阅