C# 委托与 Lambda 表达式转换机制及弱事件模式下的生命周期分析

news/2025/2/26 20:38:20/文章来源:https://www.cnblogs.com/buachuan/p/18385701

1. 委托内部结构

委托类型包含三个重要的非公共字段:

  • _target 字段

    • 静态方法包装:当委托包装一个静态方法时,该字段为 null。
    • 实例方法包装:当委托包装实例方法时,该字段引用回调方法所操作的对象。
  • _methodPtr 字段

    • 标识委托要调用的方法。
  • _invocationList 字段

    • 存储委托链(即内部委托数组),用于实现多播委托。

2. Lambda 表达式转换为委托实例

C# 编译器会将 lambda 表达式转换成相应的委托实例,具体转换方式依赖于 lambda 是否捕获外部数据。

2.1 不捕获任何外部数据

  • 转换方式

    • 将 lambda 表达式生成为私有的静态函数(编译器自动生成方法名)。
    • 同时生成一个委托类型的静态字段用于缓存委托实例。
  • 委托实例创建与缓存

    • 当调用包含 lambda 的方法时,先检查静态字段是否为 null。
    • 若不为 null,则直接返回缓存的委托实例;若为 null,则创建新的委托实例,并赋值给静态字段。
    • 这种方式确保委托实例只创建一次,被静态字段引用后不会被回收。

2.2 捕获实例成员(通过 this 访问)

  • 转换方式

    • 将 lambda 表达式生成为私有的实例函数(编译器自动生成方法名)。
  • 委托实例创建

    • 每次调用包含 lambda 的方法时,都会实时创建一个新的委托实例,包装该实例函数。

2.3 捕获非实例成员(例如局部变量)

  • 转换方式

    • 编译器生成一个私有的辅助闭包类(通常命名为 “<>c__DisplayClassXXX”)。
    • 辅助类中包含公开字段,用于保存捕获的局部变量(或其他非实例数据)。
    • 在该辅助类中,将 lambda 表达式转换为公开的实例函数,该方法通过访问辅助类字段来使用捕获的数据。
  • 委托与闭包实例的创建

    • 每次调用包含 lambda 的方法时,都会生成一个辅助类实例。
    • 然后创建一个委托实例,其 _target 字段指向该辅助类实例。
    • 注意:在循环中容易产生闭包陷阱——尽管每次迭代可能创建多个辅助类实例与委托实例,但这些辅助类实例中的捕获字段指向同一块内存(即共享同一循环变量)。由于 lambda 表达式通常在循环结束后执行,所有回调看到的循环变量值往往都是最后一次迭代的状态。
    • 另外,不同版本的 C# 对于循环中辅助类实例的创建可能存在差异,有的版本可能只在进入方法时创建一次,而有的版本则每次迭代都创建新的实例。至于委托实例,我猜测每次迭代都会创建一个新的委托实例(否则作为字典键时可能会出现重复的问题),但《CLR Via C# 第四版》中示例代码(17.7.3节,中文版365页)显示委托实例只创建了一次,这里感觉有点问题,有兴趣的朋友可以分析一下。

3. 委托实例的订阅与生命周期

3.1 常规委托/事件订阅

  • 当委托实例订阅到常规委托或事件时,事件源对委托实例持有强引用,从而延长委托实例的生命周期(直至取消订阅或事件源回收)。

3.2 弱事件订阅

  • 弱事件模式特点

    • 委托实例的生命周期至少大于其 _target 引用的对象的生命周期。
  • 实现机制

    • 利用 ConditionalWeakTable<TKey, TValue> 进行关联:
      • 将 _target 引用的对象作为 key。
      • 将委托实例作为 value。
    • ConditionalWeakTable 对 key 使用弱引用,但对 value 使用强引用,保证只要 key 存在,对应的 value 就不会被回收。
  • 订阅流程

    • 当委托实例通过 WeakEventManager<TEventSource, TEventArgs> 订阅弱事件时,内部会通过 Delegate.Target 获取 _target 引用的对象,并将该对象与委托实例关联到 ConditionalWeakTable 中,从而确保委托实例的生命周期至少与 _target 对象一致。

上面用工具重新排版了下,下面是我编辑的原文:

委托类型包含三个重要的非公共字段:_target字段,当委托实例包装一个静态方法时,该字段为空;包装实例方法时,这个字段引用回调方法要操作的对象。_methodPtr字段标识要回调的方法。_invocationList字段引用委托数组。

C#编译器将lambda方法替换为对应的委托实例。

当lambda不获取任何外部数据时,调用只创建一次委托实例并缓存:C#编译器将lambda表达式生成为私有的静态函数(编译器自动取名的方法),并生成一个委托类型的静态字段。当调用使用lambda的方法时,先判断自动生成的静态字段是否为空,不为空则直接返回静态字段引用的委托实例,为空则先创建一个包装静态函数的委托实例赋值给静态委托字段。(这导致被静态字段引用的委托实例不会被释放,但委托实例只会被创建一次)。

当lambda获取实例成员时(通过this指针访问),每次调用都创建新的委托实例:C#编译器将lambda表达式生成为私有的实例函数(编译器自动取名的方法)。每次调用使用lambda的方法时都实时创建一个委托实例包装该自动生成的实例函数。

当lambda获取非实例成员时(不通过当前实例的this指针访问,比如局部变量),C#编译器创建一个私有的辅助类,辅助类拥有对应的公开字段引用非实例成员,在辅助类中将将lambda表达式生成为公开的实例函数。每次调用使用lambda的方法时都生成辅助类实例,引用相同的非实例成员,然后创建委托实例传入辅助类实例。(循环中的闭包陷阱就在于循环中虽然创建了多个辅助类实例与委托实例,但不同辅助类实例引用的非实例成员是同一块内存。lambda 表达式是在循环中创建,但其执行往往是在循环结束后才发生,所以所有回调看到的循环变量都是最终状态。并且不同版本C#实现在循环中可能并没有创建循环次数的辅助类实例,而是在进入方法时只创建一次。我猜测创建了循环次数的委托实例,不然作为字典的键时就应该出错了。但CLR Via C#第四版给的示例代码中委托实例只创建了一次,这可能有点问题,有兴趣的朋友可以分析一下。

lambda被转换为委托实例后,当将该委托实例订阅到常规委托、事件时,事件源对委托实例进行强引用。

当将该委托实例订阅到弱事件时,存在有意思的现象:委托实例的生命周期最起码大于_target引用的对象的生命周期。这是通过ConditionalWeakTable<TKey, TValue>实现的,通过将_target引用的对象设置为key、将委托实例设置为value。该类负责数据间的关联,它对key是弱引用,但保证只要key在内存中,value就一定在内存中。

委托实例通过WeakEventManager<TEventSource, TEventArgs>订阅弱事件时,WeakEventManager<TEventSource, TEventArgs>内部会通过Delegate.Target拿到委托实例中_target引用的对象,作为ConditionalWeakTable的key,委托实例作为ConditionalWeakTable的value进行关联。这样就保证了弱事件模式下委托实例的生命周期至少大于_target引用的对象的生命周期。

public void AddHandler(Delegate handler)
{Invariant.Assert(_users == 0, "Cannot modify a ListenerList that is in use");object obj = handler.Target;if (obj == null){obj = StaticSource;}_list.Add(new Listener(obj, handler));AddHandlerToCWT(obj, handler);
}private void AddHandlerToCWT(object target, Delegate handler)
{if (!_cwt.TryGetValue(target, out var value)){_cwt.Add(target, handler);return;}List<Delegate> list = value as List<Delegate>;if (list == null){Delegate item = value as Delegate;list = new List<Delegate>();list.Add(item);_cwt.Remove(target);_cwt.Add(target, list);}list.Add(handler);
}

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/890310.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Hugging Face 发布 Python WebRTC 库:构建实时音视频应用;微软 Magma:多模态跨数字物理世界丨日报

开发者朋友们大家好:这里是 「RTE 开发者日报」 ,每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享 RTE(Real-Time Engagement) 领域内「有话题的 技术 」、「有亮点的 产品 」、「有思考的 文章 」、「有态度的 观点 」、「有看点的 活动 」,但内容仅代表编辑…

1Panel 专业版评测:全面超越宝塔的运维面板新标杆

一、 UX体验与移动端适配:更直观的跨平台交互 1Panel 专业版在用户体验上实现了对宝塔的全面超越。其界面采用现代化设计语言,以黑金主题 为代表的可定制化主题系统支持一键切换,视觉风格更符合技术审美,同时保持功能模块的清晰布局。相较于宝塔复杂的多级菜单,1Panel 的 …

基于抖音agent平台Cozi打造Agent智能体

AI Agent 一、AI Agent(智能体)与开发平台 【1】AI Agent的概念 AI Agent直接翻译更准确,AI代理人。AI Agent是指可以自主执行任务或目标的系统,它可以是一个软件,也可以是一个智能机器,这些系统通过感知环境并在此基础上做出决策。AI Agent可以集成多种技术,包括AI大模…

纯离线部署本地知识库LLM大模型

纯离线部署本地知识库LLM大模型 一、下载离线大模型下载的网址:https://hf-mirror.com/deepseek qwen 相关的模型,只建议使用1.5B的,GGUF后缀的模型 推荐下载llama相关模型,同样是GGUF后缀的,自己笔记本电脑推荐下载8B的 二、下载大模型管理平台 LM Studio下载网址:https…

mailto链接

本文主要内容转载自Haorooms!好用的mailto网站话不多说直接上正菜! 大佬dawsbot提供的工具mailto.vercel.app前言 今天看到老外写的一篇关于mailto: HTML e-mail 的文章,感觉很新奇,以前从来没有这么用过,用email的时候,最多用过mailto,但是其中穿链接参数,还是第一次看…

二次开发(第一周作业)

(1) 来源 C++ 大作业 超市收银系统 - shugen - 博客园 该文章介绍了一个软件的实现过程,并提供了相关代码。通过对该软件的分析对其进行了改进和二次开发。 (2) 运行环境+运行结果的截图(伸缩代码附上) 操作系统:Windows 11 开发工具:Dev-c++ 编程语言:C++ 运行结果截…

iconfont本地引入

本地引入iconfont的好处 Butterfly主题自带的Font Awesome图标库免费版里有好多图标没有,而阿里巴巴的开源图标iconfont里的图标非常丰富,所以可以同时引入一下iconfont图标作为Font Awesome图标库的补充。 考虑到在线引入的icon图标大小都是16x16的,而Font Awesome图标一般…

powershell-alias配置方案

最近实在被git命令和poetry命令搞烦了,每次都要输入好长的命令,并且都差不太多,所以就搜索了怎么配置alias,下面是我的配置过程,主要资料来自[1]。配置 因为我用的是Windows Terminal,主要使用的Powershell环境,所以一下教程主要是以Powershell为例,配置的Alias主要是关…

leetcode hot 13

解题思路:本题思路主要是前缀和思想,涉及到子集和问题,前缀和思想有效,两个前缀和的差就等于对应一个子集和。本题可以用哈希表的方式记录每个前缀和的值,核心依靠k = pre[j]-pre[i]转化成pre[j]-k=pre[i],计算pre[i]这个值出现的个数加到res中,最后返回。 class Solutio…

redis - [10] 持久化

redis是一个内存数据库,断电即失。需要持久化到磁盘中。 001 || RDB 在执行的时间间隔内将内存中的数据集快照写入到磁盘(快照),恢复时是将快照文件直接读到内存中。 Redis会单独创建(fork)一个子进程进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了…

小红书独立开发大赛:让你的宝藏应用被更多人看到

无论你是业余在做一款 passion project 的 builder,还是试图 bootstrapping 自己造血的小团队。小红书发起的首届独立开发大赛都值得你关注。最近越来越多的社区开发者说,他们会选择在小红书冷启动自己的项目,因为这里能获取到友善的初期反馈和精准的早期用户。这些反馈和用…

博客作业:软件缺陷分析与二次开发实践(图书管理系统)

(1)来源 本次分析的代码来源于网络上的一个C++书籍购买系统项目。该项目模拟了不同用户类型(如会员、贵宾、学生等)购买书籍时的折扣计算和购买记录保存功能。原始代码虽能运行,但存在一些设计缺陷和潜在问题。 (2)运行环境+运行结果截图 运行环境: 环境:Windows 11 +…