C#?委托與?Lambda?表達(dá)式轉(zhuǎn)換機(jī)制及弱事件模式下的生命周期詳解
1. 委托內(nèi)部結(jié)構(gòu)
委托類型包含三個(gè)重要的非公共字段:
_target 字段
- 靜態(tài)方法包裝:當(dāng)委托包裝一個(gè)靜態(tài)方法時(shí),該字段為 null。
- 實(shí)例方法包裝:當(dāng)委托包裝實(shí)例方法時(shí),該字段引用回調(diào)方法所操作的對(duì)象。
_methodPtr 字段
- 標(biāo)識(shí)委托要調(diào)用的方法。
_invocationList 字段
- 存儲(chǔ)委托鏈(即內(nèi)部委托數(shù)組),用于實(shí)現(xiàn)多播委托。
2. Lambda 表達(dá)式轉(zhuǎn)換為委托實(shí)例
C# 編譯器會(huì)將 lambda 表達(dá)式轉(zhuǎn)換成相應(yīng)的委托實(shí)例,具體轉(zhuǎn)換方式依賴于 lambda 是否捕獲外部數(shù)據(jù)。
2.1 不捕獲任何外部數(shù)據(jù)
轉(zhuǎn)換方式:
- 將 lambda 表達(dá)式生成為私有的靜態(tài)函數(shù)(編譯器自動(dòng)生成方法名)。
- 同時(shí)生成一個(gè)委托類型的靜態(tài)字段用于緩存委托實(shí)例。
委托實(shí)例創(chuàng)建與緩存:
- 當(dāng)調(diào)用包含 lambda 的方法時(shí),先檢查靜態(tài)字段是否為 null。
- 若不為 null,則直接返回緩存的委托實(shí)例;若為 null,則創(chuàng)建新的委托實(shí)例,并賦值給靜態(tài)字段。
- 這種方式確保委托實(shí)例只創(chuàng)建一次,被靜態(tài)字段引用后不會(huì)被回收。
2.2 捕獲實(shí)例成員(通過 this 訪問)
轉(zhuǎn)換方式:
- 將 lambda 表達(dá)式生成為私有的實(shí)例函數(shù)(編譯器自動(dòng)生成方法名)。
委托實(shí)例創(chuàng)建:
- 每次調(diào)用包含 lambda 的方法時(shí),都會(huì)實(shí)時(shí)創(chuàng)建一個(gè)新的委托實(shí)例,包裝該實(shí)例函數(shù)。
2.3 捕獲非實(shí)例成員(例如局部變量)
轉(zhuǎn)換方式:
- 編譯器生成一個(gè)私有的輔助閉包類(通常命名為 “<>c__DisplayClassXXX”)。
- 輔助類中包含公開字段,用于保存捕獲的局部變量(或其他非實(shí)例數(shù)據(jù))。
- 在該輔助類中,將 lambda 表達(dá)式轉(zhuǎn)換為公開的實(shí)例函數(shù),該方法通過訪問輔助類字段來使用捕獲的數(shù)據(jù)。
委托與閉包實(shí)例的創(chuàng)建:
- 每次調(diào)用包含 lambda 的方法時(shí),都會(huì)生成一個(gè)輔助類實(shí)例。
- 然后創(chuàng)建一個(gè)委托實(shí)例,其 _target 字段指向該輔助類實(shí)例。
- 注意:在循環(huán)中容易產(chǎn)生閉包陷阱——盡管每次迭代可能創(chuàng)建多個(gè)輔助類實(shí)例與委托實(shí)例,但這些輔助類實(shí)例中的捕獲字段指向同一塊內(nèi)存(即共享同一循環(huán)變量)。由于 lambda 表達(dá)式通常在循環(huán)結(jié)束后執(zhí)行,所有回調(diào)看到的循環(huán)變量值往往都是最后一次迭代的狀態(tài)。
- 另外,不同版本的 C# 對(duì)于循環(huán)中輔助類實(shí)例的創(chuàng)建可能存在差異,有的版本可能只在進(jìn)入方法時(shí)創(chuàng)建一次,而有的版本則每次迭代都創(chuàng)建新的實(shí)例。至于委托實(shí)例,我猜測(cè)每次迭代都會(huì)創(chuàng)建一個(gè)新的委托實(shí)例(否則作為字典鍵時(shí)可能會(huì)出現(xiàn)重復(fù)的問題),但《CLR Via C# 第四版》中示例代碼(17.7.3節(jié),中文版365頁(yè))顯示委托實(shí)例只創(chuàng)建了一次,這里感覺有點(diǎn)問題,有興趣的朋友可以分析一下。
3. 委托實(shí)例的訂閱與生命周期
3.1 常規(guī)委托/事件訂閱
- 當(dāng)委托實(shí)例訂閱到常規(guī)委托或事件時(shí),事件源對(duì)委托實(shí)例持有強(qiáng)引用,從而延長(zhǎng)委托實(shí)例的生命周期(直至取消訂閱或事件源回收)。
3.2 弱事件訂閱
弱事件模式特點(diǎn):
- 委托實(shí)例的生命周期至少大于其 _target 引用的對(duì)象的生命周期。
實(shí)現(xiàn)機(jī)制:
- 利用
ConditionalWeakTable<TKey, TValue>
進(jìn)行關(guān)聯(lián):- 將 _target 引用的對(duì)象作為 key。
- 將委托實(shí)例作為 value。
- ConditionalWeakTable 對(duì) key 使用弱引用,但對(duì) value 使用強(qiáng)引用,保證只要 key 存在,對(duì)應(yīng)的 value 就不會(huì)被回收。
- 利用
訂閱流程:
- 當(dāng)委托實(shí)例通過
WeakEventManager<TEventSource, TEventArgs>
訂閱弱事件時(shí),內(nèi)部會(huì)通過Delegate.Target
獲取 _target 引用的對(duì)象,并將該對(duì)象與委托實(shí)例關(guān)聯(lián)到 ConditionalWeakTable 中,從而確保委托實(shí)例的生命周期至少與 _target 對(duì)象一致。
- 當(dāng)委托實(shí)例通過
上面用工具重新排版了下,下面是我編輯的原文:
委托類型包含三個(gè)重要的非公共字段:_target字段,當(dāng)委托實(shí)例包裝一個(gè)靜態(tài)方法時(shí),該字段為空;包裝實(shí)例方法時(shí),這個(gè)字段引用回調(diào)方法要操作的對(duì)象。_methodPtr字段標(biāo)識(shí)要回調(diào)的方法。_invocationList字段引用委托數(shù)組。
C#編譯器將lambda方法替換為對(duì)應(yīng)的委托實(shí)例。
當(dāng)lambda不獲取任何外部數(shù)據(jù)時(shí),調(diào)用只創(chuàng)建一次委托實(shí)例并緩存:C#編譯器將lambda表達(dá)式生成為私有的靜態(tài)函數(shù)(編譯器自動(dòng)取名的方法),并生成一個(gè)委托類型的靜態(tài)字段。當(dāng)調(diào)用使用lambda的方法時(shí),先判斷自動(dòng)生成的靜態(tài)字段是否為空,不為空則直接返回靜態(tài)字段引用的委托實(shí)例,為空則先創(chuàng)建一個(gè)包裝靜態(tài)函數(shù)的委托實(shí)例賦值給靜態(tài)委托字段。(這導(dǎo)致被靜態(tài)字段引用的委托實(shí)例不會(huì)被釋放,但委托實(shí)例只會(huì)被創(chuàng)建一次)。
當(dāng)lambda獲取實(shí)例成員時(shí)(通過this指針訪問),每次調(diào)用都創(chuàng)建新的委托實(shí)例:C#編譯器將lambda表達(dá)式生成為私有的實(shí)例函數(shù)(編譯器自動(dòng)取名的方法)。每次調(diào)用使用lambda的方法時(shí)都實(shí)時(shí)創(chuàng)建一個(gè)委托實(shí)例包裝該自動(dòng)生成的實(shí)例函數(shù)。
當(dāng)lambda獲取非實(shí)例成員時(shí)(不通過當(dāng)前實(shí)例的this指針訪問,比如局部變量),C#編譯器創(chuàng)建一個(gè)私有的輔助類,輔助類擁有對(duì)應(yīng)的公開字段引用非實(shí)例成員,在輔助類中將將lambda表達(dá)式生成為公開的實(shí)例函數(shù)。每次調(diào)用使用lambda的方法時(shí)都生成輔助類實(shí)例,引用相同的非實(shí)例成員,然后創(chuàng)建委托實(shí)例傳入輔助類實(shí)例。(循環(huán)中的閉包陷阱就在于循環(huán)中雖然創(chuàng)建了多個(gè)輔助類實(shí)例與委托實(shí)例,但不同輔助類實(shí)例引用的非實(shí)例成員是同一塊內(nèi)存。lambda 表達(dá)式是在循環(huán)中創(chuàng)建,但其執(zhí)行往往是在循環(huán)結(jié)束后才發(fā)生,所以所有回調(diào)看到的循環(huán)變量都是最終狀態(tài)。并且不同版本C#實(shí)現(xiàn)在循環(huán)中可能并沒有創(chuàng)建循環(huán)次數(shù)的輔助類實(shí)例,而是在進(jìn)入方法時(shí)只創(chuàng)建一次。我猜測(cè)創(chuàng)建了循環(huán)次數(shù)的委托實(shí)例,不然作為字典的鍵時(shí)就應(yīng)該出錯(cuò)了。但CLR Via C#第四版給的示例代碼中委托實(shí)例只創(chuàng)建了一次,這可能有點(diǎn)問題,有興趣的朋友可以分析一下。)
lambda被轉(zhuǎn)換為委托實(shí)例后,當(dāng)將該委托實(shí)例訂閱到常規(guī)委托、事件時(shí),事件源對(duì)委托實(shí)例進(jìn)行強(qiáng)引用。
當(dāng)將該委托實(shí)例訂閱到弱事件時(shí),存在有意思的現(xiàn)象:委托實(shí)例的生命周期最起碼大于_target引用的對(duì)象的生命周期。這是通過ConditionalWeakTable<TKey, TValue>實(shí)現(xiàn)的,通過將_target引用的對(duì)象設(shè)置為key、將委托實(shí)例設(shè)置為value。該類負(fù)責(zé)數(shù)據(jù)間的關(guān)聯(lián),它對(duì)key是弱引用,但保證只要key在內(nèi)存中,value就一定在內(nèi)存中。
委托實(shí)例通過WeakEventManager<TEventSource, TEventArgs>訂閱弱事件時(shí),WeakEventManager<TEventSource, TEventArgs>內(nèi)部會(huì)通過Delegate.Target拿到委托實(shí)例中_target引用的對(duì)象,作為ConditionalWeakTable的key,委托實(shí)例作為ConditionalWeakTable的value進(jìn)行關(guān)聯(lián)。這樣就保證了弱事件模式下委托實(shí)例的生命周期至少大于_target引用的對(duì)象的生命周期。
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); }
到此這篇關(guān)于C# 委托與 Lambda 表達(dá)式轉(zhuǎn)換機(jī)制及弱事件模式下的生命周期分析的文章就介紹到這了,更多相關(guān)C# 委托與 Lambda 表達(dá)式內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C# StreamReader類實(shí)現(xiàn)讀取文件的方法
這篇文章主要介紹了C# StreamReader類實(shí)現(xiàn)讀取文件的方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01C#線性漸變畫刷LinearGradientBrush用法實(shí)例
這篇文章主要介紹了C#線性漸變畫刷LinearGradientBrush用法,實(shí)例分析了線性漸變畫刷LinearGradientBrush的相關(guān)使用技巧,需要的朋友可以參考下2015-06-06Unity向量按照某一點(diǎn)進(jìn)行旋轉(zhuǎn)
這篇文章主要為大家詳細(xì)介紹了Unity向量按照某一點(diǎn)進(jìn)行旋轉(zhuǎn),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-01-01詳解C# ConcurrentBag的實(shí)現(xiàn)原理
ConcurrentBag<T>實(shí)現(xiàn)了IProducerConsumerCollection<T>接口,該接口主要用于生產(chǎn)者消費(fèi)者模式下,可見該類基本就是為生產(chǎn)消費(fèi)者模式定制的。然后還實(shí)現(xiàn)了常規(guī)的IReadOnlyCollection<T>類,實(shí)現(xiàn)了該類就需要實(shí)現(xiàn)IEnumerable<T>、IEnumerable、 ICollection類2021-06-06Visual Studio 未能加載各種Package包的解決方案
打開Visual Studio 的時(shí)候,總提示未能加載相應(yīng)的Package包,有時(shí)候還無法打開項(xiàng)目,各種錯(cuò)誤提示,怎么解決呢?下面小編給大家?guī)砹薞isual Studio 未能加載各種Package包的解決方案,一起看看吧2016-10-10C#使用NPOI讀取excel轉(zhuǎn)為DataSet
這篇文章主要為大家詳細(xì)介紹了C#使用NPOI讀取excel轉(zhuǎn)為DataSet,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02