2.6 委托与事件:行为即数据


2.6 委托与事件:行为即数据

本节摘要:委托是类型安全的方法引用,把"待执行的行为"变成可存储、可传递的数据。本节拆解委托对象的内部三件套、多播链的执行模型、事件的封装动机与解除订阅陷阱,并交代 Lambda 到委托的编译路径——它是通往第三章 LINQ 的桥。

委托对象里装着什么

public delegate bool Predicate(int value); var isBig = new Predicate(n => n > 100); Console.WriteLine(isBig(200)); // True

第一行声明的不只是类型缩写。编译器生成了一个继承自 System.MulticastDelegate 的类,构造出的委托实例内部三件套:目标对象引用(实例方法所属对象,静态方法为 null)、方法指针(指向方法机器码)、调用列表(多播时的链表)。执行 isBig(200) 实际调用编译器生成的 Invoke 方法:沿调用链逐个以参数调用目标方法。所以委托调用与普通虚调用同量级,比反射调用(每次都要做绑定查找)便宜一到两个数量级——这个差价是第三章"反射 vs 委托"话题的基础。

函数式语言里函数是一等公民,C# 用委托达到同等表达力:把行为存进变量、放进集合、当参数传递、当返回值返回。回调、策略模式、事件、LINQ 的谓词投影,全部建立在这块基石上。C 系语言的函数指针也能做这些,但无类型检查——传错签名要到运行时崩;委托的签名在编译期就锁死,这是"类型安全的方法引用"的含义。

多播:一条链的执行语义

+- 操作的不是数字,是调用链:

Predicate check = n => n > 0; check += n => n < 1000; Console.WriteLine(check(500)); // 两个都执行,返回最后一个的结果

图 多播委托的调用链结构

图 多播委托的调用链结构

两个要点藏在图下方的注释里:有返回值的多播只保留最后一个结果(前面的返回值被丢弃,要收集结果得用 GetInvocationList 逐个调);链上任何一个节点抛异常,整条链中断——事件处理器里一个 subscriber 抛异常会殃及其他所有 subscriber,健壮的事件发布代码会逐项调用并隔离异常。

事件:给委托穿上制服

public class Stock { public event EventHandler<PriceArgs> PriceChanged; // 编译器生成:私有委托字段 + 公有 add 与 remove 访问器 }

event 关键字是一次封装手术:编译器生成一个私有委托字段和公有的 add/remove 方法(形同属性的 get/set)。封装的差别是决定性的——外部只能 +=-=,不能读列表、不能清空、不能直接 Invoke。没有 event 的公开委托字段等于把多播链暴露给全世界,任何拿到引用的代码都能偷偷清空别人的订阅。

订阅端最著名的坑是忘记退订导致对象活不过 GC:事件源持有 subscriber 的委托引用,delegate 的目标对象引用让 subscriber 无法被回收。窗体组件订阅了全局服务的事件又没在关闭时 -=,窗体对象就一辈子挂在事件链上——内存泄漏报告里这是常客。规则:生命周期短的对象订阅生命周期长的源,必须在Dispose 或关闭钩子里退订;C# 的弱事件模式或"订阅时返回一次性退订令牌"是工程化解法。

Lambda 到委托的编译路径

Func<int, int, int> add = (a, b) => a + b;

编译器在当前类生成一个私有静态方法装 Lambda 体,再把委托实例指向它——Lambda 只是匿名方法的语法糖,产物没有神秘成分。但有一个分支决定性能:捕获了外部变量的 Lambda 会生成闭包类。每个捕获的变量变成闭包类的公共字段,Lambda 变成实例方法:

int factor = 3; Func<int, int> scale = x => x * factor; // factor 被捕获 → 闭包对象被分配

这次分配在热路径上积少成多。更隐蔽的是捕获循环变量:老版本 C# 的 foreach 变量语义修订过一次,现在每次迭代是新变量,但 for 循环的 i 仍是共享的——在循环体内建 Lambda 并延迟执行,所有 Lambda 读到的是循环结束后的 i。这类"闭包捕获共享变量"的坑,本质都是同一个机制:捕获的是变量本身(通过闭包字段),不是值的快照。

C# 9 起还有一层分岔:Lambda 转成表达式树时编译产物不是 IL 方法而是描述代码结构的数据对象——这个分岔是下一节 LINQ 的核心机关,先按下不表。

本节要点回顾

  • 委托三件套:目标对象、方法指针、调用链;Invoke 沿链执行;
  • 多播的两个暗坑:只留最后返回值、单点异常中断全链;
  • event 是委托的封装:外部仅能加减订阅,发布权归类型自己;
  • 退订是订阅者的义务:不退订的对象活不过 GC;
  • Lambda 捕获即闭包:多一次分配,共享变量捕获是经典坑源。

第二章完成。下一章进入泛型、集合与 LINQ:编译器与运行时联手打造的抽象高地。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U