首先想說明一點雖然有這樣那樣的不好的心態(比如中文技術書)但總體來說國內的技術人員還是喜歡分享和教導別人的這點我的個人感受和之前在園子裡看到的朋友的感受恰恰相反我個人其實國內很多技術網友都是很熱心的可能因為語言問題同一個技術熱點會稍稍落後國外一些但一些成熟的或者基礎的概念都可以找到很細致的中文介紹特別是關於閉包因為它的字面解釋確實很繞所以基本所有試圖解釋這一名詞的同學都是盡量用自己認為最通俗易懂的方式來進行講解閒話扯遠了這裡我就用C#語言來給大家解釋下閉包吧
其實要提到閉包我們還得先提下變量作用域和變量的生命周期
在C#裡面變量作用域有三種一種是屬於類的我們常稱之為field第二種則屬於函數的我們通常稱之為局部變量還有一種其實也是屬於函數的不過它的作用范圍更小它只屬於函數局部的代碼片段這種同樣稱之為局部變量這三種變量的生命周期基本都可以用一句話來說明每個變量都屬於它所寄存的對象即變量隨著其寄存對象生而生和消亡對應三種作用域我們可以這樣說類裡面的變量是隨著類的實例化而生同時伴隨著類對象的資源回收而消亡(當然這裡不包括非實例化的static和const對象)而函數(或代碼片段)的變量也隨著函數(或代碼片段)調用開始而生伴隨函數(或代碼片段)調用結束而自動由GC釋放它內部變量生命周期滿足先進後出的特性
那麼這裡有沒有例外呢?
答案是有的不過在提這點之前我還需要給各位另外一個名詞都說c#就是MS版本的java這話 可能可以這麼說但自之後C#就可以自豪的說它絕非java了這裡面委托有很大的功勞如果用過java和C#的人並且嘗試過寫winform程序時全部手寫實現代碼的人就會有這樣一個感受同樣的click事件在java中必須要無端的套個匿名類但在c#中你是可以直接將函數名+=到事件之後而不需要顯示寫上匿名委托的對象類型的因為編譯器會幫你做這部分工作在和以後的版本之中微軟將委托的用法更是發揮的淋漓精致無論是簡潔的Lamda還是通俗易懂的LINQ都是源自委托的
你可能要問委托和我們今天要講的閉包又有什麼關系呢?
我們知道c#java和javascriptrubypython這些語言不同在c#和java的世界裡面原子對象就是類(當然還有struct和基本變量)而不是其他語言的函數我們可以實例化一個類實例化一個變量但不可以直接new 一個函數也就是表面上看我們是沒辦法像js那樣將函數進行實例化和傳遞的這也是為什麼直到Java 閉包才被姗姗來遲的加入java特性中但對C#來說這些只是表象我剛學c#的時候看到最多的解釋委托的話就是:委托啊就相當於c++裡面的函數指針啦這句話雖然籠統但卻是有一定道理通過委托特別是匿名委托這層對象的包裝我們就可以突破無法將函數當做對象傳遞的限制了
好像這裡還是沒講到閉包和委托的關系好吧我太啰嗦了下面從概念開始講
閉包其實就是使用的變量已經脫離其作用域卻由於和作用域存在上下文關系從而可以在當前環境中繼續使用其上文環境中所定義的一種函數對象
好拗口程序員還是用示例來說明更好理解
首先來個最簡單的javascript中常常見到的關於閉包的例子:
function f(){
var n=;
return function(){
alert(n);
//
return n;
}
}
var a =f();
alert(a());
這段代碼翻譯成C#代碼就是這樣:
public class TCloser
{
public Func<int> T()
{
var n = ;
return () =>
{
ConsoleWriteLine(n);
return n;
};
}
}
class Program{
static void Main(){
var a =new TCloser();
var b = aT();
ConsoleWriteLine(b());
}
}
從上面的代碼我們不難看到變量n實際上是屬於函數T的局部變量它本來生命周期應該是伴隨著函數T的調用結束而被釋放掉的但這裡我們卻在返回的委托b中仍然能調用它這裡正是閉包所展示出來的威力因為T調用返回的匿名委托的代碼片段中我們用到了n而在編譯器看來這些都是合法的因為返回的委托b和函數T存在上下文關系也就是說匿名委托b是允許使用它所在的函數或者類裡面的局部變量的於是編譯器通過一系列動作(具體動作我們後面再說)使b中調用的函數T的局部變量自動閉合從而使該局部變量滿足新的作用范圍
因此如果你看中的閉包你就可以像js中那樣理解它由於返回的匿名函數對象是在函數T中生成的因此相當於它是屬於T的一個屬性如果你把T的對象級別往上提升一個層次就很好理解了這裡就相當於T是一個類而返回的匿名對象則是T的一個屬性對屬性而言它可以調用它所寄存的對象T的任何其他屬性或者方法包括T寄存的對象TCloser內部的其他屬性如果這個匿名函數會被返回給其他對象調用那麼編譯器會自動將匿名函數所用到的方法T中的局部變量的生命周轉期自動提升並與匿名函數的生命周期相同這樣就稱之為閉合
也許你會說這個返回的委托包含的變量n只是編譯器通過某種方式隱藏的對這個委托對象的一個同樣對象的賦值吧那麼我們再對比下面兩個方法:
public class TCloser{ public Func<int> T() { var n = ; Func<int> result = () => { return n; }; n = ; return result; } public dynamic T() { var n = ; dynamic result =new { A = n }; n = ; return result; } static void Main(){ var a = new TCloser(); var b = aT(); var c = aT(); ConsoleWriteLine(b()); ConsoleWriteLine(cA); } } 最後輸出結果是什麼呢?答案是和因為閉包的特性這裡匿名函數中所使用的變量就是實際T中的變量與之相反的是匿名對象result裡面的A只是初始化時被賦予了變量n的值它並不是n所以後面n改變之後A並未隨之而改變這正是閉包的魔力所在
你可能會好本身並不支持函數對象那麼這樣的特性又是從何而來呢?答案是編譯器我們一看IL代碼便會明白了
首先我給出c#代碼:
public class TCloser { public Func<int> T(){ var n = ; return () => { return n; }; } public Func<int> T(){ return () => { var n = ; return n; }; } } 這兩個返回的匿名函數的唯一區別就是返回的委托中變量n的作用域不一樣而已T中變量n是屬於T的而在T中n則是屬於匿名函數本身的但我們看看IL代碼就會發現這裡面的大不同了:
thod public hidebysig instance class [mscorlib]SystemFunc`<int> T() cil managed{
maxstack
locals init (
[] class ConsoleApplicationTCloser/<>c__DisplayClass CS$<>__locals
[] class [mscorlib]SystemFunc`<int> CS$$)
L_: newobj instance void ConsoleApplicationTCloser/<>c__DisplayClass::ctor()
L_: stloc
L_: nop
L_: ldloc
L_: ldcis
L_a: stfld int ConsoleApplicationTCloser/<>c__DisplayClass::n
L_f: ldloc L_: ldftn instance int ConsoleApplicationTCloser/<>c__DisplayClass::<T>b__()
L_: newobj instance void [mscorlib]SystemFunc`<int>::ctor(object native int)
L_b: stloc
L_c: brs L_e
L_e: ldloc
L_f: ret
}
thod public hidebysig instance class [mscorlib]SystemFunc`<int> T() cil managed
{
maxstack
locals init (
[] class [mscorlib]SystemFunc`<int> CS$$)
L_: nop
L_: ldsfld class [mscorlib]SystemFunc`<int> ConsoleApplicationTCloser::CS$<>__CachedAnonymousMethodDelegate
L_: brtrues L_b
L_: ldnull
L_: ldftn int ConsoleApplicationTCloser::<T>b__()
L_f: newobj instance void [mscorlib]SystemFunc`<int>::ctor(object native int)
L_: stsfld class [mscorlib]SystemFunc`<int> ConsoleApplicationTCloser::CS$<>__CachedAnonymousMethodDelegate
L_: brs L_b
L_b: ldsfld class [mscorlib]SystemFunc`<int> ConsoleApplicationTCloser::CS$<>__CachedAnonymousMethodDelegate
L_: stloc
L_: brs L_
L_: ldloc
L_: ret
}
看IL代碼你就會很容易發現其中究竟了在T中函數對返回的匿名委托構造的是一個類名稱為newobj instance void ConsoleApplicationTCloser/<>c__DisplayClass::ctor()而在T中則是仍然是一個普通的Func委托只不過級別變為類級別了而已
那我們接著看看T中聲明的類c__DisplayClass是何方神聖:
class auto ansi sealed nested private beforefieldinit <>c__DisplayClass extends [mscorlib]SystemObject{
custom instance void [mscorlib]SystemRuntimeCompilerServicesCompilerGeneratedAttribute::ctor()
thod public hidebysig specialname rtspecialname instance void ctor() cil managed{}
thod public hidebysig instance int <T>b__() cil managed{}
field public int n }
看到這裡想必你已經明白了在C#中原來閉包只是編譯器玩的花招而已它仍然沒有脫離NET對象生命周期的規則它將需要修改作用域的變量直接封裝到返回的類中變成類的一個屬性n從而保證了變量的生命周期不會隨函數T調用結束而結束因為變量n在這裡已經成了返回的類的一個屬性了
看到這裡我想大家應該大體上了解閉包的來龍去脈了吧閉包其實和類中其他屬性方法是一樣的它們的原則都是下一層可以暢快的調用上一層定義的各種設定但上一層則不具備訪問下一層設定的能力即類中方法裡的變量可以自由訪問類中的所有屬性和方法而閉包又可以訪問它的上一層即方法中的各種設定但類不可以訪問方法的局部變量同理方法也不可以訪問其內部定義的匿名函數所定義的局部變量
這正是C#中的閉包它通過超越java語言的委托打下了閉包的第一步基礎隨後又通過各種語法糖和編譯器來實現如今在NET世界全面開花的Lamda和LINQ也使得我們能夠編寫出更加簡潔優雅的代碼
附:後面是吐槽與上文無關大家可以略過這篇文章其實兩年之前在給同事講C#閉包的時候就有想法整理出來和大家分享了不過因為生活工作或許主要還是自己太懶的原因而拖著沒動筆到今天早上看到園友抱怨國內教書育人的氛圍才最終決定利用晚上時間把它整理然後放出來我個人認為國內技術圈子的氛圍尚可雖然仍然很多浮躁和易怒在圈子裡徘徊但我們想想國內IT人的生存空間就容易理解了每天最理想的情況朝晚的干活晚上加班周末加班這些都是常事而對我們而言只要想寫出一些經過細細思考的東西都至少需要個小時以上而且最好中間不要有人來打擾這也就注定我們在白天工作時候很難完全有時間靜下來組織語言刨掉這些時間留給我們自己的生活時間又有多少呢?所以我每次看到有園友發表帖子的時間是晚上點點甚至更晚都毫不意外
我們並非專業寫手也不像國外IT人那樣有充足的閒暇時光可以鑽研自己的最愛我們賺著他們的零頭買著比他們本子價格更貴的筆記本擔著比他們更高房價的壓力來生活這樣的生活條件下我們這些可愛的社區(不僅限於cnblogsjavaeyephpchina等)Geek們仍然如此活躍和熱情你還能抱怨什麼呢?你要知道你看到的每篇文章(如果是工作人士的話)都是他們晚上從點寫到點的生活點滴啊
From:http://tw.wingwit.com/Article/program/net/201311/12547.html