C# 核心机制面试:委托、事件、Lambda 表达式与闭包
本章聚焦 C# 面试中高频出现的委托、事件、Lambda 与闭包四组概念。面试官常从“是什么→怎么用→底层原理→延伸陷阱”层层深入。掌握下图中的回答框架,能帮你快速组织逻辑,避开常见扣分点。
🎯 图注: 回答时先讲定义,再对比事件对委托的封装、Lambda 对匿名方法的简化,最后指出闭包捕获外部变量可能引发内存泄漏,并主动提及线程安全问题。按此路径递进,面试官会认为你有完整的知识链条。
📌 面试问答卡片
Q1:委托和事件有什么区别?
面试官考察点: 是否理解事件是委托的封装,能否讲出“只能+=/-=”的限制以及外部直接赋值的危险性。
回答框架: ① 委托是方法签名的引用类型,可任意赋值和调用;② 事件是委托的安全封装,外部只能 add/remove;③ 事件在类内部可触发,外部不可直接 Invoke。
示例回答(80字):事件是委托的“阉割版”,在外界只能通过 +=/-= 绑定/解除,不能直接赋值或 Invoke,从而保护了封装性。内部仍可像委托一样调用。这是设计模式观察者模式的 C# 实现基础。
可能追问: 自定义事件和标准事件有何不同?事件是否线程安全?
避坑提醒: ❌ 别说“事件就是委托”,必须强调封装和限制;❌ 不要忘记提线程安全——事件触发前应做空检查(?.Invoke)。
Q2:Lambda 表达式中的闭包可能导致什么问题?如何避免?
面试官考察点: 对闭包捕获变量机制的理解,以及内存泄漏、循环引用的排查能力。
回答框架: ① 闭包捕获的是变量的引用而非值;② 若捕获大型对象或静态资源,可能导致 GC 无法回收;③ 常见场景:事件注册后未注销;④ 避免:及时注销、使用弱引用、避免在循环中创建闭包。
示例回答(85字):闭包捕获外部变量时若该变量生命周期较长(如 UI 控件),会导致委托对象长期存活,造成内存泄漏。解决:在窗体关闭时 -= 事件;或使用 WeakReference 包装捕获变量;for 循环中捕获迭代变量应复制副本。
可能追问: 如何用弱引用实现闭包?.NET 的 WeakEvent 模式了解吗?
避坑提醒: ❌ 不能简单回答“闭包捕获的是值类型副本”;✅ 要区分捕获变量与闭包生成的对象存活关系。
Q3:解释委托的协变和逆变。
面试官考察点: 对泛型委托变体的理解,以及实际编码中泛型委托的灵活使用。
回答框架: ① 协变(out):返回值类型可替换为基类型;② 逆变(in):参数类型可替换为派生类型;③ 典型应用:Func
示例回答(75字):协变指委托返回类型可为基类,如 Func<string> 可赋值给 Func<object>;逆变指参数类型可为派生类,如 Action<object> 可赋值给 Action<string>。它们让泛型委托在继承体系中更灵活。
可能追问: 普通委托不支持变体吗?自定义泛型委托如何标记变体?
避坑提醒: ❌ 别混淆数组协变(可能有类型安全问题)与委托协变;✅ 强调只适用于引用类型。
🏗️ 项目案例章节:委托与事件实战模板
| 项目名称 | 内容 |
|---|---|
| 分布式事件总线系统 |
业务背景: 微服务架构下,服务间通过事件进行异步通信,需要解耦且支持动态订阅。 规模指标: 日均事件量 5000 万+,订阅方 200+,延迟 < 100ms。 技术栈: .NET 6 / RabbitMQ / 自定义委托事件总线 / ConcurrentDictionary 缓存订阅。 核心难点: 大量委托订阅导致内存泄漏、线程安全触发、事件执行顺序。 面试高频追问: - 如何设计事件总线中的委托缓存? - 如何保证事件执行顺序和异常隔离? - 批量取消订阅如何优化性能? 可迁移回答话术: “我们使用 ConcurrentDictionary<Type, WeakReference<Delegate>> 管理订阅,避免强引用导致内存泄漏;事件触发时采用 Task.Run 异步且捕获异常不中断整体;支持按类型批量取消时先快照再遍历,防止枚举修改异常。”
|
⚠️ 面试加分点: 在讲述项目时主动抛出“WeakReference + 委托”的组合方案,并提及 EventHandler 的线程安全调用习惯(?.Invoke),会让面试官认为你有生产级别的实践经验。