淺談C#閉包的相關(guān)原理
首先想說明一點,雖然有這樣那樣的不好的心態(tài)(比如中文技術(shù)書),但總體來說,國內(nèi)的技術(shù)人員還是喜歡分享和教導(dǎo)別人的,這點我的個人感受和之前在園子里看到的朋友的感受恰恰相反.我個人其實國內(nèi)很多技術(shù)網(wǎng)友都是很熱心的,可能因為語言問題同一個技術(shù)熱點會稍稍落后國外一些,但一些成熟的或者基礎(chǔ)的概念都可以找到很細致的中文介紹,特別是關(guān)于閉包,因為它的字面解釋確實很繞,所以基本所有試圖解釋這一名詞的同學(xué)都是盡量用自己認為最通俗易懂的方式來進行講解.閑話扯遠了,這里我就用C#語言來給大家解釋下閉包吧。
其實要提到閉包,我們還得先提下變量作用域和變量的生命周期。
在C#里面,變量作用域有三種,一種是屬于類的,我們常稱之為field,第二種則屬于函數(shù)的,我們通常稱之為局部變量,還有一種,其實也是屬于函數(shù)的,不過它的作用范圍更小,它只屬于函數(shù)局部的代碼片段,這種同樣稱之為局部變量。這三種變量的生命周期基本都可以用一句話來說明,每個變量都屬于它所寄存的對象,即變量隨著其寄存對象生而生和消亡.對應(yīng)三種作用域我們可以這樣說,類里面的變量是隨著類的實例化而生,同時伴隨著類對象的資源回收而消亡(當(dāng)然這里不包括非實例化的static和const對象).而函數(shù)(或代碼片段)的變量也隨著函數(shù)(或代碼片段)調(diào)用開始而生,伴隨函數(shù)(或代碼片段)調(diào)用結(jié)束而自動由GC釋放,它內(nèi)部變量生命周期滿足先進后出的特性。
那么這里有沒有例外呢?
答案是有的,不過在提這點之前,我還需要給各位另外一個名詞.都說c#就是MS版本的java,這話在.net 1.0可能可以這么說,但自2.0之后C#就可以自豪的說它絕非java了,這里面委托有很大的功勞,如果用過java和C#的人并且嘗試過寫winform程序時全部手寫實現(xiàn)代碼的人就會有這樣一個感受,同樣的click事件,在java中必須要無端的套個匿名類,但在c#中,你是可以直接將函數(shù)名+=到事件之后而不需要顯示寫上匿名委托的對象類型的,因為編譯器會幫你做這部分工作,在3.0和以后的版本之中,微軟將委托的用法更是發(fā)揮的淋漓精致,無論是簡潔的Lamda還是通俗易懂的LINQ,都是源自委托的.
你可能要問,委托和我們今天要講的閉包又有什么關(guān)系呢?
我們知道,c#,java和javascript,ruby,python這些語言不同,在c#和java的世界里面,原子對象就是類(當(dāng)然還有struct和基本變量),而不是其他語言的函數(shù),我們可以實例化一個類,實例化一個變量,但不可以直接new 一個函數(shù).也就是表面上看,我們是沒辦法像js那樣將函數(shù)進行實例化和傳遞的.這也是為什么直到Java 7閉包才被姍姍來遲的加入java特性中。但對C#來說這些只是表象,我剛學(xué)c#的時候,看到最多的解釋委托的話就是:委托啊,就相當(dāng)于c++里面的函數(shù)指針啦.這句話雖然籠統(tǒng),但卻是有一定道理,通過委托特別是匿名委托這層對象的包裝,我們就可以突破無法將函數(shù)當(dāng)做對象傳遞的限制了.
好像這里還是沒講到閉包和委托的關(guān)系,好吧,我太啰嗦了,下面從概念開始講.
閉包其實就是使用的變量已經(jīng)脫離其作用域,卻由于和作用域存在上下文關(guān)系,從而可以在當(dāng)前環(huán)境中繼續(xù)使用其上文環(huán)境中所定義的一種函數(shù)對象.
好拗口,程序員,還是用示例來說明更好理解.
首先來個最簡單的javascript中常常見到的關(guān)于閉包的例子:
- function f1(){
- var n=999;
- return function(){
- alert(n); // 999
- return n;
- }
- }
- var a =f1();
- alert(a());
這段代碼翻譯成C#代碼就是這樣:
- public class TCloser
- {
- public Func<int> T1()
- {
- var n = 999;
- return () =>
- {
- Console.WriteLine(n);
- return n;
- };
- }
- }
- class Program{
- static void Main(){
- var a =new TCloser();
- var b = a.T1();
- Console.WriteLine(b());
- }
- }
從上面的代碼我們不難看到,變量n實際上是屬于函數(shù)T1的局部變量,它本來生命周期應(yīng)該是伴隨著函數(shù)T1的調(diào)用結(jié)束而被釋放掉的,但這里我們卻在返回的委托b中仍然能調(diào)用它,這里正是閉包所展示出來的威力,因為T1調(diào)用返回的匿名委托的代碼片段中我們用到了n,而在編譯器看來,這些都是合法的,因為返回的委托b和函數(shù)T1存在上下文關(guān)系,也就是說匿名委托b是允許使用它所在的函數(shù)或者類里面的局部變量的,于是編譯器通過一系列動作(具體動作我們后面再說)使b中調(diào)用的函數(shù)T1的局部變量自動閉合,從而使該局部變量滿足新的作用范圍。
因此如果你看到.net中的閉包,你就可以像js中那樣理解它,由于返回的匿名函數(shù)對象是在函數(shù)T1中生成的,因此相當(dāng)于它是屬于T1的一個屬性。如果你把T1的對象級別往上提升一個層次就很好理解了,這里就相當(dāng)于T1是一個類,而返回的匿名對象則是T1的一個屬性,對屬性而言,它可以調(diào)用它所寄存的對象T1的任何其他屬性或者方法,包括T1寄存的對象TCloser內(nèi)部的其他屬性。如果這個匿名函數(shù)會被返回給其他對象調(diào)用,那么編譯器會自動將匿名函數(shù)所用到的方法T1中的局部變量的生命周轉(zhuǎn)期自動提升并與匿名函數(shù)的生命周期相同,這樣就稱之為閉合。
也許你會說,這個返回的委托包含的變量n只是編譯器通過某種方式隱藏的對這個委托對象的一個同樣對象的賦值吧,那么我們再對比下面兩個方法:
- public class TCloser{
- public Func<int> T1()
- {
- var n = 999;
- Func<int> result = () =>
- {
- return n;
- };
- n = 10;
- return result;
- }
- public dynamic T2()
- {
- var n = 999;
- dynamic result =new { A = n };
- n = 10;
- return result;
- }
- static void Main(){
- var a = new TCloser();
- var b = a.T1();
- var c = a.T2();
- Console.WriteLine(b());
- Console.WriteLine(c.A);
- }
- }
***輸出結(jié)果是什么呢?答案是10和999,因為閉包的特性,這里匿名函數(shù)中所使用的變量就是實際T1中的變量,與之相反的是,匿名對象result里面的A只是初始化時被賦予了變量n的值,它并不是n,所以后面n改變之后A并未隨之而改變。這正是閉包的魔力所在。
你可能會好奇.net本身并不支持函數(shù)對象,那么這樣的特性又是從何而來呢?答案是編譯器,我們一看IL代碼便會明白了。
首先我給出c#代碼:
- public class TCloser {
- public Func<int> T1(){
- var n = 10;
- return () =>
- {
- return n;
- };
- }
- public Func<int> T4(){
- return () =>
- {
- var n = 10;
- return n;
- };
- }
- }
這兩個返回的匿名函數(shù)的唯一區(qū)別就是返回的委托中變量n的作用域不一樣而已,T1中變量n是屬于T1的,而在T4中,n則是屬于匿名函數(shù)本身的。但我們看看IL代碼就會發(fā)現(xiàn)這里面的大不同了:
- .method public hidebysig instance class [mscorlib]System.Func`1<int32> T1() cil managed{
- .maxstack 3
- .locals init (
- [0] class ConsoleApplication1.TCloser/<>c__DisplayClass1 CS$<>8__locals2,
- [1] class [mscorlib]System.Func`1<int32> CS$1$0000)
- L_0000: newobj instance void ConsoleApplication1.TCloser/<>c__DisplayClass1::.ctor()
- L_0005: stloc.0
- L_0006: nop
- L_0007: ldloc.0
- L_0008: ldc.i4.s 10
- L_000a: stfld int32 ConsoleApplication1.TCloser/<>c__DisplayClass1::n
- L_000f: ldloc.0
- L_0010: ldftn instance int32 ConsoleApplication1.TCloser/<>c__DisplayClass1::<T1>b__0()
- L_0016: newobj instance void [mscorlib]System.Func`1<int32>::.ctor(object, native int)
- L_001b: stloc.1
- L_001c: br.s L_001e
- L_001e: ldloc.1
- L_001f: ret
- }
- .method public hidebysig instance class [mscorlib]System.Func`1<int32> T4() cil managed
- {
- .maxstack 3
- .locals init (
- [0] class [mscorlib]System.Func`1<int32> CS$1$0000)
- L_0000: nop
- L_0001: ldsfld class [mscorlib]System.Func`1<int32> ConsoleApplication1.TCloser::CS$<>9__CachedAnonymousMethodDelegate4
- L_0006: brtrue.s L_001b
- L_0008: ldnull
- L_0009: ldftn int32 ConsoleApplication1.TCloser::<T4>b__3()
- L_000f: newobj instance void [mscorlib]System.Func`1<int32>::.ctor(object, native int)
- L_0014: stsfld class [mscorlib]System.Func`1<int32> ConsoleApplication1.TCloser::CS$<>9__CachedAnonymousMethodDelegate4
- L_0019: br.s L_001b
- L_001b: ldsfld class [mscorlib]System.Func`1<int32> ConsoleApplication1.TCloser::CS$<>9__CachedAnonymousMethodDelegate4
- L_0020: stloc.0
- L_0021: br.s L_0023
- L_0023: ldloc.0
- L_0024: ret
- }
看IL代碼你就會很容易發(fā)現(xiàn)其中究竟了,在T1中,函數(shù)對返回的匿名委托構(gòu)造的是一個類,名稱為newobj instance void ConsoleApplication1.TCloser/<>c__DisplayClass1::.ctor(),而在T4中,則是仍然是一個普通的Func委托,只不過級別變?yōu)轭惣墑e了而已。
那我們接著看看T1中聲明的類c__DisplayClass1是何方神圣:
- .class auto ansi sealed nested private beforefieldinit <>c__DisplayClass1
- extends [mscorlib]System.Object{
- .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
- .method public hidebysig specialname rtspecialname instance void .ctor() cil managed{}
- .method public hidebysig instance int32 <T1>b__0() cil managed{}
- .field public int32 n
- }
看到這里想必你已經(jīng)明白了,在C#中,原來閉包只是編譯器玩的花招而已,它仍然沒有脫離.NET對象生命周期的規(guī)則,它將需要修改作用域的變量直接封裝到返回的類中變成類的一個屬性n,從而保證了變量的生命周期不會隨函數(shù)T1調(diào)用結(jié)束而結(jié)束,因為變量n在這里已經(jīng)成了返回的類的一個屬性了。
看到這里我想大家應(yīng)該大體上了解閉包的來龍去脈了吧,閉包其實和類中其他屬性、方法是一樣的,它們的原則都是下一層可以暢快的調(diào)用上一層定義的各種設(shè)定,但上一層則不具備訪問下一層設(shè)定的能力。即類中方法里的變量可以自由訪問類中的所有屬性和方法,而閉包又可以訪問它的上一層即方法中的各種設(shè)定。但類不可以訪問方法的局部變量,同理,方法也不可以訪問其內(nèi)部定義的匿名函數(shù)所定義的局部變量。
這正是C#中的閉包,它通過超越j(luò)ava語言的委托打下了閉包的***步基礎(chǔ),隨后又通過各種語法糖和編譯器來實現(xiàn)如今在.NET世界全面開花的Lamda和LINQ.也使得我們能夠編寫出更加簡潔優(yōu)雅的代碼。
附:后面是吐槽,與上文無關(guān),大家可以略過,這篇文章其實兩年之前在給同事講C#閉包的時候就有想法整理出來和大家分享了,不過因為生活,工作,或許主要還是自己太懶的原因而拖著沒動筆,到今天早上看到園友抱怨國內(nèi)教書育人的氛圍才最終決定利用晚上時間把它整理,然后放出來。我個人認為國內(nèi)技術(shù)圈子的氛圍尚可,雖然仍然很多浮躁和易怒在圈子里徘徊。但我們想想國內(nèi)IT人的生存空間就容易理解了。每天最理想的情況朝9晚6的干活,晚上加班,周末加班這些都是常事,而對我們而言,只要想寫出一些經(jīng)過細細思考的東西都至少需要2個小時以上,而且***中間不要有人來打擾,這也就注定我們在白天工作時候很難完全有時間靜下來組織語言,刨掉這些時間,留給我們自己的生活時間又有多少呢?所以我每次看到有園友發(fā)表帖子的時間是晚上1點,2點甚至更晚,都毫不意外,
我們并非專業(yè)寫手,也不像國外IT人那樣有充足的閑暇時光可以鉆研自己的***,我們賺著他們的零頭,買著比他們本子價格更貴的筆記本,擔(dān)著比他們更高房價的壓力來生活,這樣的生活條件下我們這些可愛的社區(qū)(不僅限于cnblogs,javaeye,phpchina等)Geek們?nèi)匀蝗绱嘶钴S和熱情,你還能抱怨什么呢?你要知道你看到的每篇文章(如果是工作人士的話)都是他們晚上從9點寫到12點的生活點滴啊。
所以,以后不要抱怨國內(nèi)IT氛圍吧,相對這個社會其他各行各業(yè)的浮躁,我覺得我們的IT圈子已經(jīng)是很樂于分享的一個群體了。而且除了因為“天下武功,源自歐美,滯后于英語國家”的緣故,我們有些技術(shù)確實要晚些才能跟上國外社區(qū)的腳步,但對于一些基礎(chǔ)知識的解釋,已經(jīng)有很多中文的文章解釋得很不錯了。像我以前在理解閉包的時候, javaeye上看到的一大堆,像WIKI,像阮一峰的文章,我個人認為對中文用戶是足夠了。當(dāng)然,這只是我個人的觀點,大家不必較勁。
原文鏈接:http://www.cnblogs.com/frankfang/archive/2011/08/03/2125663.html