恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

C# delegate与event、Action、Func、EventHandler:从原理到实战选型

  • 首页
  • 资讯中心
  • /
  • C# delegate与event、Action、Func、EventHandler:从原理到实战选型

相关资讯

OpenJDK 11.0.24 Linux生产部署指南:ZGC、容器内存与安全基线 2026/10/16 20:03:46
普中HC6800-ES V2.0开发板全外设实操指南:串口/ADC/EEPROM闭环验证 2026/10/16 20:03:46
POS收银开发避坑指南:从DLL封装到支付回调的完整调试手册 2026/10/16 20:03:46

最新资讯

机器学习学习器分类与实战:监督与无监督算法选型指南
C#桌面端麦克风录音实战:NAudio方案选型、参数配置与避坑指南
CAD坐标标注插件zbbz:解决工程图纸坐标失真与协同落地难题
考前30天,如何高效利用四六级翻译预测题PDF
AI Agent技术解析:核心概念、系统架构与工程落地指南
EO.WebBrowser:.NET WinForms项目替换IE内核的Chromium浏览器控件

今日推荐

Java八种基本类型深度解析:从内存布局到实战选型
基础:常用快捷键——IDEA、VS Code、PyCharm 与 Linux 终端效率速查
解决ultraedit下verilog .v文件中文注释乱码:把编码配置改到TaoToken辅助排查

本周热门

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

C# delegate与event、Action、Func、EventHandler:从原理到实战选型

发布时间:2026/10/16 20:03:46
C# delegate与event、Action、Func、EventHandler:从原理到实战选型 1. 从四个看着很像的东西说起先问个直接的问题delegate和event长得像Action和EventHandler也长得像——你在实际项目里到底怎么选我在带团队的时候发现这是C#面试里出现频率极高、但真正能讲清楚的人不超过三成的话题。大多数人的状态是会用Action知道event能挂方法但为什么要有这四种东西它们之间到底是什么关系就说不利索了。这篇文章我想把这条线彻底捋直。我会从delegate的本质讲起因为它是一切的地基然后讲event为什么被单独设计成一个关键字再讲Action和Func这两个语法糖替我们省了多少事最后拆开EventHandler看看.NET标准事件处理模式到底在规范什么。这四条线走完你以后再看到任何事件的代码心里都会有一张清晰的图。这篇文章适合谁刚入门C#、被委托和事件绕晕的新手写了两三年业务代码但对底层设计意图不够清晰的开发者还有面试前想把这个知识点系统梳理一遍的朋友。我不打算写成一板一眼的文档而是按我自己的理解把为什么这么设计讲透。2. 地基delegate到底是个什么东西2.1 委托是类型不是方法很多新手第一次接触委托脑子里会冒出一个模糊的印象委托好像就是把方法当参数传来传去。这个印象没错但太浅了。真正的要害是——委托是一种类型就像int、string、class一样它是一个数据类型。只不过这个类型存的是方法的引用而不是数值或对象。我们看一段最基础的代码public delegate void SayHelloDelegate(string name); class Program { static void ChineseGreeting(string name) { Console.WriteLine($你好{name}); } static void EnglishGreeting(string name) { Console.WriteLine($Hello, {name}); } static void Main() { SayHelloDelegate greet ChineseGreeting; greet(张三); greet EnglishGreeting; greet(张三); } }留意第三行public delegate void SayHelloDelegate(string name);。这一行的含义是——我声明了一个新的类型这个类型的名字叫SayHelloDelegate凡是这个类型的变量都可以指向一个接收string参数、返回void的方法。下面SayHelloDelegate greet ChineseGreeting;就是实例化这个类型把ChineseGreeting方法的引用存进greet变量里。这个过程跟int x 5;本质上是同一回事只是x装的是数字greet装的是方法。如果你的第一反应是那我能不能把任何方法都塞给委托——不行。委托是强类型的只能接收签名匹配的方法。想要装接收两个int返回int的方法就得声明成public delegate int SomeDelegate(int a, int b);这种形式。2.2 多播委托一个委托变量挂多个方法这里有一个关键能力实战中几乎天天用到——委托变量的可以把多个方法挂到同一个委托上。调用一次这些方法按挂载顺序依次执行。public delegate void NotifyDelegate(string message); class Program { static void Main() { NotifyDelegate notifier null; notifier LogToFile; notifier LogToConsole; notifier SendEmail; notifier(系统启动完成); } static void LogToFile(string msg) { /* 写入日志文件 */ } static void LogToConsole(string msg) { /* 输出控制台 */ } static void SendEmail(string msg) { /* 发邮件 */ } }这个特性让委托天然就是事件的雏形——多个订阅者各自挂一个处理方法发布者一声令下全部执行。但要注意两个细节。第一个细节的顺序决定执行顺序这个顺序是确定的别指望并发。第二个细节如果委托方法的返回值不是void那么多播调用时只能拿到最后一个方法的返回值前面所有方法的返回值都被丢弃了。这个问题怎么解决有两种思路。一是用GetInvocationList()把委托拆成单个调用列表逐个手动调用、逐个取返回值public delegate int CalculateDelegate(int x); class Program { static void Main() { CalculateDelegate calc null; calc x x 1; calc x x * 2; foreach (CalculateDelegate single in calc.GetInvocationList()) { Console.WriteLine(single(10)); } } }这样每个方法的返回值都能拿到。第二种思路是干脆约定所有委托方法都返回void用引用类型参数传结果。事件领域里标准做法基本是后者。2.3 匿名方法和Lambdas的关系C# 2.0引入了匿名方法C# 3.0引入了Lambda表达式。有了Lambda之后声明一个独立的具名方法来匹配委托在很多场景下显得笨重// 传统写法必须声明一个具名方法 static bool IsEven(int num) num % 2 0; Listint evens numbers.Where(IsEven).ToList(); // Lambda写法就地定义一个匿名函数 Listint evens2 numbers.Where(num num % 2 0).ToList();底层其实一样Lambda表达式本质上是编译器帮我们生成的一个匿名方法甚至可能生成一个闭包类来捕获外部变量然后交给委托。核心不变——委托类型的变量承载的还是方法的引用。Lambda只是便捷写法不是新概念。3. event为什么有了delegate还要再造一个关键字3.1 event对delegate的限制这是我让团队里每个人都要背下的一句话event是一个受限制的delegate它在delegate的基础上增加了封装防止外部代码滥用委托的灵活性。看个实际例子。假设我们定义一个广播器类public class Broadcaster { // 直接暴露委托字段 public Actionstring OnMessage; // 用event封装 public event Actionstring MessageEvent; public void Broadcast(string msg) { OnMessage?.Invoke(msg); MessageEvent?.Invoke(msg); } }坏就坏在public Actionstring OnMessage这个字段上。委托字段是public的意味着类的外部可以做三件事var b new Broadcaster(); // 1. 直接调用委托——绕过Broadcaster自己的触发逻辑 b.OnMessage(外部直接调用); // 2. 用粗暴覆盖之前挂载的所有方法 b.OnMessage msg Console.WriteLine(我把之前的订阅全清了); // 3. 获取InvocationList查看内部有哪些方法 var list b.OnMessage.GetInvocationList();这三件事在事件语义下全部是禁区。事件应该遵循发布者触发、订阅者响应的模型订阅者只挂载方法和取消挂载毫无理由去直接调用、覆盖或检查发布者的事件列表。把OnMessage改成event Actionstring MessageEvent之后上述三种操作全部编译报错。类的外部只能做和-。这是event最大的价值强制约束使用边界把委托变量变成事件通道。3.2 event的编译真相很多文章讲到这里就停了但我还要往深挖一层——event在编译器层面到底做了什么C#编译器会把event字段编译成私有委托字段外加一对add和remove访问器类似属性的get/set。我们写的在编译后其实是调用add访问器-则是调用remove访问器。好消息是C#已经帮我们实现好了这对访问器的默认版本日常不需要手动写。但如果你有特殊需求——比如想把事件订阅记录存到别的地方而不是简单委托链里——你是可以手动写add/remove的就像手动实现属性一样。还有一件事要注意编译器对event字段默认生成的是委托链而委托链上的每个操作在并发环境下不是线程安全的。如果多个线程同时订阅同一个事件可能发生丢失。极端情况下需要自己加锁或用Interlocked.CompareExchange来保证。中小型项目里通常没人管这个但你要知道这个坑在哪。3.3 触发事件的规范copy-invoke触发事件的标准姿势其实是有一番讲究的。看这两种写法// 糟糕的写法检查、触发两行之间订阅者可能被别的线程移除 if (MessageEvent ! null) { MessageEvent(hello); } // 标准写法先拷贝到局部变量再判断、再触发 var handler MessageEvent; if (handler ! null) { handler(hello); }第二种写法在C# 6.0以后可以简化为一行MessageEvent?.Invoke(hello);?.会先对MessageEvent执行一次读取值拷贝再判空、再调用恰好规避了竞态问题。这也是为什么我们在现代C#代码里看到的事件触发基本都是SomeEvent?.Invoke(...)。这不是装酷是为了安全。4. Action与Func泛型委托时代的语法糖4.1 从手写delegate到直接用现成泛型我在第2节里自己声明了SayHelloDelegate、NotifyDelegate、CalculateDelegate。你发现没有——每次都要为不同签名搞一个新委托类型很烦。尤其是项目大了以后委托类型命名都得想半天。微软也烦了于是从.NET 3.5开始在System命名空间下内置了一批通用委托类型。核心就两个Action系列用于无返回值的方法。从Action无参到ActionT1, T2, ...最多支持16个泛型参数返回值一律是void。Func系列用于有返回值的方法。最后一个泛型参数约定是返回类型比如FuncT, TResult——接收一个T类型参数返回TResult。支持到16个输入参数加1个返回值。于是第2节那段代码直接变成Actionstring greet ChineseGreeting; greet(张三); Funcint, int, int add (a, b) a b; Console.WriteLine(add(3, 5));是不是清爽得多现在我们写业务代码几乎不需要自己声明委托类型了。听到有人说Action是一个委托没错——它是System.Action这个泛型委托类型本质还是委托。4.2 Action多播与Func返回值的小坑Action是多播委托可以挂多个方法这跟第2节讲的多播行为完全一致。Func同样可以挂多个方法但别忘了那条规则——多播Func只返回最后一个方法的结果Funcint f () 1; f () 2; f () 3; Console.WriteLine(f()); // 输出3前面的1和2被丢弃我在代码评审里见过有人拿多播Func做责任链结果只拿到最后一步的返回值前面几步全白算了。要拿全部分段结果还是得走GetInvocationList()手动遍历。所以我的建议是多播场景优先使用Action返回值场景使用单个Func别指望两者功能叠加。4.3 Predicate和其他类型的取舍除了Action和Func还有PredicateT——接收一个T返回bool常用于List.FindAll这类方法。本质上PredicateT和FuncT, bool是等价的只是语义上强调了判断的意图。选择标准很直接语义先行。做事件通知、命令分发用Action做查询、映射、转换用Func做集合筛选条件用Predicate。大家都是一个底层机制选哪个纯粹从代码可读性出发。5. EventHandler把事件和数据打包的行业标准5.1 一个加引号的标准事件模式前面讲了event如何封装委托Action如何简化委托声明——现在把两者放一起你会很自然地写出public event Actionstring MessageReceived;这段代码能跑完全没问题。但我得说这还不是.NET世界的标准事件模式。标准模式长这样public event EventHandler MessageReceived; public event EventHandlerMessageEventArgs MessageReceivedWithData;EventHandler是干什么的它是一个预定义的委托类型签名固定为public delegate void EventHandler(object sender, EventArgs e); public delegate void EventHandlerTEventArgs(object sender, TEventArgs e);核心规矩有两条第一个参数永远是sender表示事件源——谁触发了这个事件第二个参数是事件数据——EventArgs是所有事件参数类的基类如果不需要传任何额外数据就用EventArgs.Empty需要传数据就自定义一个继承EventArgs的类再泛型化。为什么这样设计想想实际场景。UI按钮被点击订阅者需要知道是哪个按钮发的。如果你只用Action这个信息得靠闭包捕获或者额外参数传递绕。但标准事件模式用sender字段把事件源头直接暴露出来订阅者可以判断if (sender buttonA)。再加上事件数据类信息完整又清晰。5.2 用标准事件模式实现一个完整案例我把标准事件模式完整过一遍。假设做一个模拟下载器下载进度变化时向外发通知// 1. 自定义事件数据类继承EventArgs public class DownloadProgressEventArgs : EventArgs { public int Percent { get; } public string FileName { get; } public DownloadProgressEventArgs(int percent, string fileName) { Percent percent; FileName fileName; } } // 2. 发布者类 public class Downloader { // 值变化时触发 public event EventHandlerDownloadProgressEventArgs ProgressChanged; public void Start(string fileName) { for (int i 0; i 100; i 10) { Thread.Sleep(200); ProgressChanged?.Invoke(this, new DownloadProgressEventArgs(i, fileName)); } } } // 3. 订阅者 class Program { static void Main() { var downloader new Downloader(); // 注意订阅方法的签名必须匹配 (object sender, DownloadProgressEventArgs e) downloader.ProgressChanged OnProgressChanged; downloader.Start(setup.exe); } static void OnProgressChanged(object sender, DownloadProgressEventArgs e) { Console.WriteLine(${e.FileName} 下载进度{e.Percent}%); } }注意两点。第一ProgressChanged?.Invoke(this, ...)的sender传的是this——事件源就是下载器实例本身。如果事件是从内部子组件转发出来的sender应该传那个子组件而不是转发者。第二订阅方法的签名必须严格匹配EventHandlerDownloadProgressEventArgs——不是你Delegate签名的都能挂只有这种两参数签名能挂。5.3 EventHandler vs Action的直接对比我在实际带项目时见过一个典型的二选一混乱。有人问public event Actionstring MessageReceived;和public event EventHandlerMessageEventArgs MessageReceived;选哪个我的判断规则是这样的对比维度event Actionevent EventHandler适用范围内部模块通信、轻量通知、不需要事件源信息类库设计、控件/组件事件、需要跨模块复用参数风格直接传业务数据简洁sender 事件数据类信息完整语义约束弱谁都能自定义签名强全.NET统一模式兼容性仅限当前项目内部约定可对接.NET UI控件、ASP.NET生命周期等标准事件代码量少需要多写一个EventArgs子类结论如果你在写的一个类将来可能被其他项目、其他团队复用遵循标准事件模式是底线。如果你只是在项目内部做模块间消息传递event Actionstring更轻量。从设计规范的角度说UI框架和通用类库无一例外都用了EventHandler这本身就说明了标准的力量。6. 四个概念的精确关系与一个实战选型清单6.1 一张图看明白它们的关系不用画复杂结构图了直接说结论delegate是通配的地基。所有委托类型的本质都是方法引用的封装。就算你不用delegate关键字Action、Func、EventHandler底层也都是委托。event是受限的委托字段。它通过add/remove访问器对外只暴露订阅和退订本质上是对委托的一种封装策略。Action和Func是预定义好的泛型委托。它们帮你省去手动声明委托类型的负担Action对应无返回值方法Func对应有返回值方法。EventHandler是事件 标准参数约定的合体。它强制了(sender, e)两参数模式让所有事件订阅和发布遵循统一规范。一句话总结delegate定义机制event定义封装规则Action/Func提供便捷类型EventHandler给出标准范式。四者不是互相替代的关系而是层层递进的关系。6.2 实战选型决策清单结合我在实际项目里的经验整理一份决策清单你遇到类似场景可以直接对号入座场景推荐方案原因方法回调比如异步完成后通知ActionT或FuncT, TResult简洁无发布/订阅模型需求多步骤处理管道每个操作处理完传给下一个Func输入, 输出链每步输出作为下一步输入模块内部事件通知比如状态变更event Action状态类型轻量团队内约定一致即可类库/组件对外暴露事件event EventHandlerTEventArgs符合.NET标准调用方容易上手安全地暴露委托回调不允许多播普通delegate字段 空判断明确单播语义观察者模式多订阅者广播event EventHandler天然多播 封装约束6.3 还有一个常见疑惑delegate能和event互换吗有人问既然委托可以是public字段那我直接public Action和public event Action唯一区别不就是外部不能吗对但又不对。最大的区别在于语义。对外暴露public Action等于告诉调用者这个你可以随便调、随便覆盖、随便清空。而public event Action等于告诉调用者这个你只能订阅和退订。一个类是组件的入口还是消息中枢从成员声明方式就能看出来。好的API设计靠类型系统就把用法约束住了而不是靠文档写请不要手动调用。我见过一个实际事故某开发者为了图省事把内部通知字段写成public Action结果外部模块在初始化时用覆盖了通知链导致这个模块里所有订阅者静默失效排查了大半天才定位到。用event把边界守住这类问题根本不会出现。7. 从命名到线程安全事件开发的最后几块拼图7.1 事件命名的通用惯例事件命名和一般的属性方法命名不同惯例是用动词的过去式或进行时表示已经发生或正在发生。常见有这些用户点击按钮Clicked加载完成Loaded/LoadCompleted数据变化Changed/PropertyChanged值校验前Validating进行时表示即将做某事数据类后缀统一加EventArgs比如UserLoginEventArgs然后事件声明为EventHandlerUserLoginEventArgs。这套命名不是强制的但遵循它整个团队的代码风格会非常统一阅读一个陌生模块时几乎不用猜。7.2 事件与GC谁引用了谁最容易出错订阅事件造成的内存泄漏是C#里著名的暗坑。事件发布者持有对订阅者的强引用也就是说只要发布者还活着被订阅的方法所在的订阅者对象就永远无法被垃圾回收。假设一个业务服务对象orderService是单例长期存活而某个一次性界面订阅了orderService.StockChanged事件。界面销毁时如果忘了-这个界面对象其实一直挂在orderService的事件链上永远回收不了。界面每打开一次就挂一个内存就像漏水的桶。所以原则很硬谁订阅谁负责退订。短生命周期的订阅者必须在Dispose或卸载流程里执行-。这不是可选优化是必须写进代码规范里的事项。7.3 线程安全事件触发的完整写法有了?.Invoke事件触发本身在引用丢失这个意义上的竞争是安全的。但如果你的订阅处理方法不是线程安全的那么两个线程同时触发同一个事件订阅方法可能会被并发执行这时需要你自己在方法内部处理锁。事件机制本身不承诺任何并发安全。另外在UI线程编程时事件订阅方法里更新控件可能需要封送回UI线程——这些属于事件处理的延伸功课了先知道有这层以后踩到再回来补。8. 代码评审中的高频重灾区这几条红线别踩8.1 用比较委托等于没比较委托的比较不是用去判断两个变量里的方法引用是否值相等吗严格来说这个操作基本没有意义。委托变量是一个链表头比较的是引用或值而不是方法签名。如果你要判断这个事件是否订阅了某方法正确做法是遍历GetInvocationList()逐个与目标方法比较bool alreadySubscribed false; if (downloader.ProgressChanged ! null) { foreach (Delegate single in downloader.ProgressChanged.GetInvocationList()) { if (single.Target this single.Method.Name OnProgressChanged) { alreadySubscribed true; break; } } }尽管理论上可以这样查但我更建议从设计上避免这种需求——事件订阅关系由业务代码保证不要临时去查表。8.2 前置判空后仍去Invoke很多人知道要判空但会把判空和调用分开写比如if (evt ! null) { // ...中间隔了好几行业务逻辑... evt(...); }这是教科书级的错——中间如果出了异常或者有其他代码把事件链改了你就踩到空引用了。判空和调用必须连成一个原子操作。现代C#就用evt?.Invoke(...)一行搞定没必要自己写if。我评审代码时看到这样的写法会直接标记为需要修改。8.3 多播顺序依赖既然顺序决定了执行顺序那么你就不应该依赖这个顺序来完成先处理A再处理B的逻辑。今天你写代码时顺序是 A然后 B看起来没问题。等三个月后有人重构在前面插了个 C行为就变了。如果有顺序要求正确的做法是在一个订阅方法内部显式处理阶段顺序而不是依赖订阅顺序这种隐式约定。9. 拆一个真实案例完整走一遍设计过程最后我用一个完整案例把前面所有知识点串起来。场景做一个简单的登录状态管理器需要在登录成功后通知多个模块刷新状态。这是我在给团队做培训时经常用的例子——因为它麻雀虽小五脏俱全。第一步定义事件数据类public class LoginEventArgs : EventArgs { public string UserId { get; } public bool IsSuccess { get; } public LoginEventArgs(string userId, bool isSuccess) { UserId userId; IsSuccess isSuccess; } }第二步定义登录管理器public class LoginManager { // 对外只暴露订阅/退订能力 public event EventHandlerLoginEventArgs LoginCompleted; public void Login(string userId, string password) { bool success CheckPassword(userId, password); // 触发事件不管成败都通知 LoginCompleted?.Invoke(this, new LoginEventArgs(userId, success)); } private bool CheckPassword(string userId, string password) { // 模拟校验 return password 123456; } }第三步订阅端的标准写法class UiModule { private readonly LoginManager _loginManager; public UiModule(LoginManager loginManager) { _loginManager loginManager; // 订阅事件 _loginManager.LoginCompleted OnLoginCompleted; } private void OnLoginCompleted(object sender, LoginEventArgs e) { if (e.IsSuccess) { Console.WriteLine($欢迎{e.UserId}); // 刷新用户信息、加载菜单、更新标题等 } else { Console.WriteLine(登录失败请重试); } } // 模块销毁时务必退订 public void Destroy() { _loginManager.LoginCompleted - OnLoginCompleted; } }这个案例里只用了event EventHandlerTEventArgs一条路就把前面所有点都覆盖了委托是底层机制EventHandler本身是委托类型、事件关键字保证了封装边界、数据类继承了EventArgs、sender传了事件发布者this、事件命名用了过去式Completed。订阅端要退订其实就是把换成-。如果你在项目里照着这个模式写类库的公共API基本就是符合行业标准的。有人会问那Action和Func在这个登录例子里完全没用吗也不是。比如LoginManager内部可以用Funcstring, string, bool注入密码校验策略或者用ActionLoginEventArgs做内部通知管道。它们是同一个底层机制在不同粒度上的应用。我个人在这十年里踩过很多坑最后形成一个习惯代码里凡是多方联动、生命周期可能不同步的场景一律用标准事件模式加显式退订凡是一次性回调用Action/Func。这个习惯让出现内存泄漏和意外调用的概率降到了很低。希望这篇梳理也能帮你把这条线彻底打通以后遇到事件相关代码一眼就能看穿它在什么层级上工作。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号