C#與閉包

來源:互聯網
上載者:User

首先想說明一點,雖然有這樣那樣的不好的心態(比如中文技術書),但總體來說,國內的技術人員還是喜歡分享和教導別人的,這點我的個人感受和之前在園子裡看到的朋友的感受恰恰相反.個人認為其實國內很多技術網友都是很熱心的,可能因為語言問題同一個技術熱點會稍稍落後國外一些,但一些成熟的或者基礎的概念都可以找到很細緻的中文介紹,特別是關於閉包,因為它的字面解釋確實很繞,所以基本所有試圖解釋這一名詞的同學都是盡量用自己認為最通俗易懂的方式來進行講解.閑話扯遠了,這裡我就用C#語言來給大家解釋下閉包吧.

其實要提到閉包,我們還得先提下變數範圍和變數的生命週期.
在C#裡面,變數範圍有三種,一種是屬於類的,我們常稱之為field,第二種則屬於函數的,我們通常稱之為局部變數,還有一種,其實也是屬於函數的,不過它的作用範圍更小,它只屬於函數局部的程式碼片段,這種同樣稱之為局部變數.這三種變數的生命週期基本都可以用一句話來說明,每個變數都屬於它所寄存的對象,即變數隨著其寄存對象生而生和消亡.對應三種範圍我們可以這樣說,類裡面的變數是隨著類的執行個體化而生,同時伴隨著類對象的資源回收而消亡(當然這裡不包括非執行個體化的static和const對象).而函數(或程式碼片段)的變數也隨著函數(或程式碼片段)調用開始而生,伴隨函數(或程式碼片段)調用結束而自動由GC釋放,它內部變數生命週期滿足先進後出的特性。

那麼這裡有沒有例外呢?
答案是有的,不過在提這點之前,我還需要給各位另外一個名詞.都說c#就是MS版本的java,這話在.net 1.0可能可以這麼說,但自2.0之後C#就可以自豪的說它絕非java了,這裡面委託有很大的功勞,如果用過java和C#的人並且嘗試過寫winform程式時全部手寫實現代碼的人就會有這樣一個感受,同樣的click事件,在java中必須要無端的套個匿名類,但在c#中,你是可以直接將函數名+=到事件之後而不需要顯示寫上匿名委託的物件類型的,因為編譯器會幫你做這部分工作,在3.0和以後的版本之中,微軟將委託的用法更是發揮的淋漓精緻,無論是簡潔的Lamda還是通俗易懂的LINQ,都是源自委託的.

你可能要問,委託和我們今天要講的閉包又有什麼關係呢?
我們知道,c#,java和javascript,ruby,python這些語言不同,在c#和java的世界裡面,原子物件就是類(當然還有struct和基本變數),而不是很多動態語言中的函數,我們可以執行個體化一個類,執行個體化一個變數,但不可以直接new 一個函數.也就是表面上看,我們是沒辦法像js那樣將函數進行執行個體化和傳遞的.這也是為什麼直到Java 7閉包才被姍姍來遲的加入java特性中。但對C#來說這些只是表象,我剛學c#的時候,看到最多的解釋委託的話就是:委託啊,就相當於c++裡面的函數指標啦.這句話雖然籠統,但確實有一定道理,通過委託特別是匿名委託這層對象的封裝,我們就可以突破無法將函數當做對象傳遞的限制了.

好像這裡還是沒講到閉包和委託的關係,好吧,我太囉嗦了,下面從概念開始講.
閉包其實就是使用的變數已經脫離其範圍,卻由於和範圍存在上下文關係,從而可以在當前環境中繼續使用其上文環境中所定義的一種函數對象.
好拗口,程式員,還是用樣本來說明更好理解.
首先來個最簡單的javascript中常常見到的關於閉包的例子:

    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實際上是屬於函數T1的局部變數,它本來生命週期應該是伴隨著函數T1的調用結束而被釋放掉的,但這裡我們卻在返回的委託b中仍然能調用它,這裡正是閉包所展示出來的威力,因為T1調用返回的匿名委託的程式碼片段中我們用到了n,而在編譯器看來,這些都是合法的,因為返回的委託b和函數T1存在上下文關係,也就是說匿名委託b是允許使用它所在的函數或者類裡面的局部變數的,於是編譯器通過一系列動作(具體動作我們後面再說)使b中調用的函數T1的局部變數自動閉合,從而使該局部變數滿足新的作用範圍。
    因此如果你看到.net中的閉包,你就可以像js中那樣理解它,由於返回的匿名函數對象是在函數T1中產生的,因此相當於它是屬於T1的一個屬性。如果你把T1的對象層級往上提升一個層次就很好理解了,這裡就相當於T1是一個類,而返回的匿名對象則是T1的一個屬性,對屬性而言,它可以調用它所寄存的對象T1的任何其他屬性或者方法,包括T1寄存的對象TCloser內部的其他屬性。如果這個匿名函數會被返回給其他對象調用,那麼編譯器會自動將匿名函數所用到的方法T1中的局部變數的生命周轉期自動提升並與匿名函數的生命週期相同,這樣就稱之為閉合。
   
    也許你會說,這個返回的委託包含的變數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);    }} 

    最後輸出結果是什麼呢?答案是10和999,因為閉包的特性,這裡匿名函數中所使用的變數就是實際T1中的變數,與之相反的是,匿名對象result裡面的A只是初始化時被賦予了變數n的值,它並不是n,所以後面n改變之後A並未隨之而改變。這正是閉包的魔力所在。
   
    你可能會好奇.net本身並不支援函數對象,那麼這樣的特性又是從何而來呢?答案是編譯器,我們一看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;            };        }    }

    這兩個返回的匿名函數的唯一區別就是返回的委託中變數n的範圍不一樣而已,T1中變數n是屬於T1的,而在T4中,n則是屬於匿名函數本身的。但我們看看IL代碼就會發現這裡面的大不同了:
   

.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代碼你就會很容易發現其中究竟了,在T1中,函數對返回的匿名委託構造的是一個類,名稱為newobj instance void ConsoleApplication1.TCloser/<>c__DisplayClass1::.ctor(),而在T4中,則是仍然是一個普通的Func委託,只不過層級變為類層級了而已。
那我們接著看看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}

看到這裡想必你已經明白了,在C#中,原來閉包只是編譯器玩的花招而已,它仍然沒有脫離.NET對象生命週期的規則,它將需要修改範圍的變數直接封裝到返回的類中變成類的一個屬性n,從而保證了變數的生命週期不會隨函數T1調用結束而結束,因為變數n在這裡已經成了返回的類的一個屬性了。

看到這裡我想大家應該大體上瞭解C#閉包的來龍去脈了吧,C#中,閉包其實和類中其他屬性、方法是一樣的,它們的原則都是下一層可以暢快的調用上一層定義的各種設定,但上一層則不具備訪問下一層設定的能力。即類中方法裡的變數可以自由訪問類中的所有屬性和方法,而閉包又可以訪問它的上一層即方法中的各種設定。但類不可以存取方法的局部變數,同理,方法也不可以訪問其內部定義的匿名函數所定義的局部變數。

這正是C#中的閉包,它通過超越java語言的委託打下了閉包的第一步基礎,隨後又通過各種文法糖和編譯器來實現如今在.NET世界全面開花的Lamda和LINQ.也使得我們能夠編寫出更加簡潔優雅的代碼。

附:後面是吐槽,與上文無關,大家可以略過,這篇文章其實兩年之前在給同事講C#閉包的時候就有想法整理出來和大家分享了,不過因為生活,工作,或許主要還是自己太懶的原因而拖著沒動筆,到今天早上看到園友抱怨國內教書育人的氛圍才最終決定利用晚上時間把它整理,然後放出來。我個人認為國內技術圈子的氛圍尚可,雖然仍然很多浮躁和易怒在圈子裡徘徊。但我們想想國內IT人的生存空間就容易理解了。每天最理想的情況朝9晚6的幹活,晚上加班,周末加班這些都是常事,而對我們而言,只要想寫出一些經過細細思考的東西都至少需要2個小時以上,而且最好中間不要有人來打擾,這也就註定我們在白天工作時候很難完全有時間靜下來組織語言,刨掉這些時間,留給我們自己的生活時間又有多少呢?所以我每次看到有園友發表文章的時間是晚上1點,2點甚至更晚,都毫不意外,
我們並非專業寫手,也不像國外IT人那樣有充足的閑暇時光可以鑽研自己的最愛,我們賺著他們的零頭,買著比他們本子價格更貴的筆記本,擔著比他們更高樓價的壓力來生活,這樣的生活條件下我們這些可愛的社區(不僅限於cnblogs,javaeye,phpchina等)Geek們仍然如此活躍和熱情,你還能抱怨什麼呢?你要知道你看到的每篇文章(如果是工作人士的話)都是他們晚上從9點寫到12點的生活點滴啊。

所以,以後不要抱怨國內IT氛圍吧,相對這個社會其他各行各業的浮躁,我覺得我們的IT圈子已經是很樂於分享的一個群體了。而且除了因為“天下武功,源自歐美,滯後於英語國家”的緣故,我們有些技術確實要晚些才能跟上國外社區的腳步,但對於一些基礎知識的解釋,已經有很多中文的文章解釋得很不錯了。像我以前在理解閉包的時候, javaeye上看到的一大堆,像WIKI,像阮一峰的文章,我個人認為對中文使用者是足夠了。當然,這隻是我個人的觀點,大家不必較勁。

最後一點抱怨就是國內大大小小的抄襲網站,我想這也是影響我們中文使用者查詢資料的一個重要因素吧。以前曾經嘗試過在baidu,google上搜尋自己的文章,但結果相當令人失望,那些抄襲的網站從來都不在乎內容,因為這些可以通過抄來解決,而且不必帶原文連結,不必表明作者。好像東西就是他們自己的一樣,他們唯一在乎的就是SEO。這也導致我在使用google搜尋的時候時常看到同一篇文章出現在某一頁的所有搜尋結果中,當然,網址是千奇百怪,實在讓人無奈。有些網站即使標明了出處和作者,但用心略有險惡的不是給的連結,而是文字。而且,在這些抄襲者中,最讓我感到悲哀的是大名鼎鼎的敗毒文庫,我至少看到不下5篇敗毒文庫裡的文章是來自JE或者CSDN的,但在文庫裡面只有個文檔,你看不到任何作者提示或者原文連結,也許有人會說,這也可能是作者自己上傳的呀,但我個人認為這種可能性太小了,以國內IT人的風格,對敗毒即使談不上厭惡,也很少有主動去巴巴的,試想,一個國內最大的互連網公司都不尊重IT人的勞動(但願我是錯的吧),你又能對其他人說什麼呢? 同樣看看國外,就我看到的DZone, WindowsPhoneGeek等,每個都是很明確的給出原文的連結,基本上我很少看到有引用別人文章不給原文連結的文章的。而正是這些不尊重作者勞動的網站對國內互連網資料搜尋造成大量的垃圾資訊。

引用:
維基百科 http://zh.wikipedia.org/wiki/%E9%97%AD%E5%8C%85_%28%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6%29
部落格園 http://www.cnblogs.com/frankfang/archive/2011/08/03/2125663.html

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.