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