ToB企服应用市场:ToB评测及商务社交产业平台
标题:
C# 委托与 Lambda 表达式转换机制及弱变乱模式下的生命周期分析
[打印本页]
作者:
张国伟
时间:
3 天前
标题:
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 进行关联:
将 _target 引用的对象作为 key。
将委托实例作为 value。
ConditionalWeakTable 对 key 利用弱引用,但对 value 利用强引用,保证只要 key 存在,对应的 value 就不会被回收。
订阅流程
:
当委托实例通过 WeakEventManager 订阅弱变乱时,内部会通过 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实现的,通过将_target引用的对象设置为key、将委托实例设置为value。该类负责数据间的关联,它对key是弱引用,但保证只要key在内存中,value就一定在内存中。
委托实例通过WeakEventManager订阅弱变乱时,WeakEventManager内部会通过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);
}
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/)
Powered by Discuz! X3.4