C#由變數捕獲引起對閉包的思考

發表於2016-06-23

前言

偶爾翻翻書籍看看原理性的東西確實有點枯燥,之前有看到園中有位園友說到3-6年工作經驗的人應該瞭解的.NET知識,其中就有一點是關於C#中的閉包,其實早之前在看書時(之前根本不知道C#中還有閉包這一說)看到對於閉包的內容篇幅很少而且介紹的例子一看就懂(最終也就是有個印象而已),反正工作又用不到來讓你去實現閉包,於是乎自己心存僥倖心理,這兩天心血來潮再次翻了翻書想仔細研究一番(或許是出於內心的惶恐吧,工作幾年竟然不知道閉包,就算知道而且僅止於瞭解,你是在自欺欺人麼),緊接著就查了下資料來研究研究這個東西,若有錯誤之處,請指出。

話題

首先來我們來看看委託的演變進化史,有人問了,本節的主題不是【C#由變數捕獲引起對閉包的思考】?哦,看的還仔細,是的,我們能彆著急麼,又有人問了,你之前不是寫過有關委託的詳細介紹麼,哦,看來還是我的粉絲知道的還挺多,但是這個介紹側重點不同啦,廢話少來,進入主題才是真理。

C#1.0之delegate

我們今天知道過來一個列表可以通過lamda如where或者predicate來實現,而在C#1.0中我們必須來寫一個方法來實現predicate的邏輯,緊接著建立委託例項是通過指定的方法名來建立。我們來建立一個幫助類(ListUtil),如下:

同時給出一個測試資料:

現在我們要做的是返回其長度小等於4的字串並列印,給出長度小於4的方法:

下面我們在控制檯呼叫上述方法來進行過濾:

結果列印如下:

上述一切都是so easy!當我們利用委託來實現時只是簡單的進行一次呼叫不會多次用到,為了精簡程式碼,此時匿名方法出現在C# 2.0.

C#2.0之delegate

上述在控制檯進行呼叫方法我們稍作修改即可達到同樣效果,如下:

好了,到了這裡貌似有點浪費篇幅,到這裡我們反觀上述程式碼,對於predicate中過濾資料長度都是硬編碼,缺少點什麼,我們首先要講的是閉包,那對於閉包需要的可以基本概括為:閉包是函式與其引用環境組合而成的實體(來源於:你必須知道的.NET)。我們可以將其理解為函式與上下文的綜合。我們需要來通過手動輸入過濾資料的長度來給出一個上下文。我們給出一個過濾長度的類(VariableLengthMather):

下面我們來進行手動輸入呼叫以此來過濾資料:

演示如下:

接著我們將上述控制檯程式碼進行如下改造:

我們只是將maxLength值改變了下,再次進行列印,演示結果如下:

C#3.0之delegate

為了更好的演示程式碼,我們利用C#3.0中lamda表示式來進行演示,我們繼續接著上述來講,當我們將maxLength修改為5時,此時過濾的資料和4一樣,此時我們利用匿名方法或者lamda表示式同樣進行如上演示,如下。

看看演示結果:

從上述演示結果可以看出此時的maxLength為5,當然列印過濾的結果則不一樣,這個時候就得說到第一個話題【變數捕獲】。在C# 2.0和3.0中的匿名方法和lambda表示式都能捕獲到本地變數。

那麼問題來了,什麼是變數捕獲呢?我們怎麼去理解呢?

我們接下來利用lambda表示式再來看一個例子:

那麼列印的結果將會是什麼呢?cnblogs?xpy0928?

name被捕獲,當本地變數發生改變時lambda也同樣作出對應的改變(因lambda會延遲執行),所以會輸出xpy0928。那麼編譯器到底做了什麼才使得輸出xpy0928呢?編譯器內部將上述程式碼進行了大致如下轉換。

到了這裡想必我們能夠理解了捕獲變數的意義lambda始終指向當前物件中的name值即物件中的引用始終存在於lamda中。

接下來就要說到閉包,在此之前一直在討論變數捕獲,因為閉包產生的源頭就是變數捕獲。(個人理解,若有錯誤請指正)。

變數捕獲的結果就是編譯器將產生一個物件並將該區域性變數提升為例項變數從而達到延長區域性變數的生命週期,儲存到的這個物件叫做所謂的閉包。

上述這句話又是什麼意思?我們再來看一個例子:

有人說上述例子就是閉包,對,是閉包且結果返回10個10,恩完事!不能就這樣吧,我們還得解釋清楚。我們一句一句來看。

()=>j代表什麼意思,來我們來看看之前lambda表示式的六部進化曲:

例項化一個匿名委託並返回j值,注意這裡說()=>j是返回變數j的當前值而非返回值j。返回的匿名委託為一個匿名類並訪問此類中的屬性j(為什麼說返回的匿名委託為一個匿名類,請看此連結:http://www.cnblogs.com/jujusharp/archive/2011/08/04/C-Sharp-And-Closure.html) 。好說完這裡,我們再來解釋為什麼列印10個10?

此時建立的每個匿名委託都捕獲了這個變數j,所以每個匿名委託即匿名類保持了對欄位j的引用,當for迴圈完畢時即10時此時欄位值全變為10,直到j不被匿名委託所引用,j才被會垃圾回收器回收。

我們再來看看對上述進行修改:

很明顯將輸出0-9因為此時建立了一個臨時變數tempJ,當每次進行迭代時匿名委託即lambda捕獲的是不同的tempJ,所以此時能按照我們預期所輸出。

我們再來看一種情況:

此時還是正常輸出0-9,因為此時lambda表示式每次迭代完立即執行,而不像第一個例子直到延遲到迴圈到10才開始進行所有的lambda執行。

總結

閉包概念:閉包允許你將一些行為封裝,將它像一個物件一樣傳來遞去,而且它依然能夠訪問到原來第一次宣告時的上下文。這樣可以使控制結構、邏輯操作等從呼叫細節中分離出來。

作用:(1)利於程式碼精簡。(2)利於函式程式設計。(3)程式碼安全。

參考資料:

C# in Depth:http://csharpindepth.com/Articles/Chapter5/Closures.aspx

Variable Capture in C# with Anonymous Delegates:http://www.digitallycreated.net/Blog/34/variable-capture-in-c%23-with-anonymous-delegates

Understanding Variable Capturing in C#:https://blogs.msdn.microsoft.com/matt/2008/03/01/understanding-variable-capturing-in-c/

C#與閉包:http://www.cnblogs.com/jujusharp/archive/2011/08/04/C-Sharp-And-Closure.html 

相關文章