C# 委托与 Lambda 表达式转换机制及弱事件模式下的生命周期分析
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);
}
C# 委托与 Lambda 表达式转换机制及弱事件模式下的生命周期分析的更多相关文章
- 《C#本质论》读书笔记(12)委托和Lambda表达式
12.1.委托概述 12.1.2 委托的数据类型 为了减少重复代码数量,可以将比较方法作为参数传递给 BubbleSort()方法.此外,为了将方法作为参数传递,必须有一个能够标识方法的数据类型--也 ...
- 十二、C# 委托与Lambda表达式(匿名方法的另一种写法)
委托与Lambda表达式 1.委托概述 2.匿名方法 3.语句Lambda 4.表达式Lambda 5.表达式树 一.委托概述 相当于C++当中的方法指针,在C#中使用delegate 委托来 ...
- C# Note2:委托(delegate) & Lambda表达式 & 事件(event)
前言 本文主要讲述委托和Lambda表达式的基础知识,以及如何通过Lambda表达式实现委托调用,并阐述.NET如何将委托用作实现事件的方式. 参考:C#高级编程 1.什么是委托(delegate)? ...
- C# 委托 (一)—— 委托、 泛型委托与Lambda表达式
C# 委托 (一)—— 委托. 泛型委托与Lambda表达式 2018年08月19日 20:46:47 wnvalentin 阅读数 2992 版权声明:此文乃博主之原创.鄙人才疏,望大侠斧正.此 ...
- 转载 C#匿名函数 委托和Lambda表达式
转载原出处: http://blog.csdn.net/honantic/article/details/46331875 匿名函数 匿名函数(Anonymous Function)是表示“内联”方法 ...
- 系统预定义委托与Lambda表达式
NET中那些所谓的新语法之三:系统预定义委托与Lambda表达式 开篇:在上一篇中,我们了解了匿名类.匿名方法与扩展方法等所谓的新语法,这一篇我们继续征程,看看系统预定义委托(Action/Fun ...
- C#函数式程序设计之函数、委托和Lambda表达式
C#函数式程序设计之函数.委托和Lambda表达式 C#函数式程序设计之函数.委托和Lambda表达式 相信很多人都听说过函数式编程,提到函数式程序设计,脑海里涌现出来更多的是Lisp.Haske ...
- 匿名委托与Lambda表达式
通过使用匿名委托(匿名方法),使编程变得更加灵活,有关委托与匿名委托请参考我的前一篇Blog<委托与匿名委托>. 继续之前示例,代码如下: static void Main(string[ ...
- 深入学习C#匿名函数、委托、Lambda表达式、表达式树类型——Expression tree types
匿名函数 匿名函数(Anonymous Function)是表示“内联”方法定义的表达式.匿名函数本身及其内部没有值或者类型,但是可以转换为兼容的委托或者表达式树类型(了解详情).匿名函数转换的计算取 ...
- 委托学习过程及委托、Lambda表达式和匿名方法的关系总结及事件总结
第一章,当开始学习委托的时候,我们会问什么是委托?为什么要学习委托? 一,什么是委托? 委托是一个类,它定义了方法的类型,使得可以将方法当作另一个方法的参数来进行传递,这种将方法动态地赋给参数的做法, ...
随机推荐
- 腾讯云对象存储COS获Veritas认证,数据安全能力再升级
近日获悉,腾讯云对象存储 COS 正式通过 Veritas 备份软件标准化测试,为数据安全再添新助力. Veritas 对 COS 的支持已经从底层打通,目前 Veritas 的 NetBackup ...
- simpleui
目录 一.simpleui 1.1 使用步骤 1.2 功能介绍 1.3 展示大屏 一.simpleui 之前公司里,做项目前后端结合,要使用权限,要快速搭建后台管理,使用djagno的admin直接搭 ...
- 常用bat代码
清除空文件夹 | 清理空文件夹 | 删除空文件夹 @echo off for /f "tokens=*" %%i in ('dir/s/b/ad^|sort /r') do rd ...
- 【Linux】CentOS7 远程联机
# CentOS7 远程联机 哔哩哔哩 萌狼蓝天 博客:https://mllt.cc 微信公众号:萌狼蓝天 检查与安装配置OpenSSH [CentOS7]检查系统是否安装OpenSSH yum - ...
- 【MyBatis】学习笔记08:批量删除
[Mybatis]学习笔记01:连接数据库,实现增删改 [Mybatis]学习笔记02:实现简单的查 [MyBatis]学习笔记03:配置文件进一步解读(非常重要) [MyBatis]学习笔记04:配 ...
- Qt编写视频监控系统76-Onvif跨网段组播搜索和单播搜索的实现
一.前言 在视频监控行业一般会用国际onvif工具来测试设备是否支持onvif协议,工具的名字叫ONVIF Device Manager(还有个工具叫ONVIF Device Test Tool,专用 ...
- Qt编写物联网管理平台32-表格数据
一.前言 用表格来展示采集到的数据,是很多组态系统中最常见的方法,一个表格能够展示的数据特别多,在本系统中,默认做的也是通过表格的形式来展示数据,目前是将所有的设备放在一个表格中,后期可能按照不同控制 ...
- Qt编写安防视频监控系统66-子模块10网页浏览
一.前言 网页浏览模块,用于传入一个网页地址,打开对应的网页进行浏览,可用于网页展示信息,支持多个,可以自行增加,代码中演示了一个.此模块的用途属于添砖加瓦润色用的,比如有一个牛逼的3D网页,机器人. ...
- (三).NET6.0使用Autofac实现依赖注入
1.添加依赖注入的两个关键包 Autofac.Extensions.DependencyInjection 和 Autofac.Extras.DynamicProxy 2.在Program中添加Aut ...
- 一步一步abp电商模块-1、搭建模块环境
前言 目前在开发abp电商模块,打算做一步,写一步,算是对自己的记录,主要是参考nopcommoner 并结合abp模块开发 知识都是连贯的,如果你熟悉asp.net core 3.x.abp(非vN ...