如果一個程序設計語言能夠用高階函數解決問題,則意味著數據作用域問題已十分突出。當函數可以當成參數和返回值在函數之間進行傳遞時,編譯器利用閉包擴展變量的作用域,以保證隨時能得到所需要的數據。 C#函數式程序設計之作用域 在C#中,變量的作用域是嚴格確定的。其本質是所有代碼生存在類的方法中、所有變量只生存於聲明它們的模塊中或者之後的代碼中。變量的值是可變的,一個變量越是公開,帶來的問題就越嚴重。一般的原則是,變量的值最好保持不變,或者在最小的作用域內保存其值。一個純函數最好只使用在自己的模塊中定義的變量值,不訪問其作用域之外的任何變量。 遺憾的是,有時我們無法把變量的值限制於函數的范圍內。如果在程序的初始化時定義了幾個變量,在後面需要反復用到它們,怎麼辦?一個可能的辦法是使用閉包。 C#函數式程序設計之閉包機制 為了理解閉包的本質,我們分析幾個使用閉包的例子: namespace Closures { class Closures { static void Closures() { Console.WriteLine(GetClosureFunc()(30)); } static Func<int,int> GetClosureFunc() { int val = 10; Func<int, int> internalAdd = x => x + val; Console.WriteLine(internalAdd(10)); val = 30; Console.WriteLine(internalAdd(10)); return internalAdd; } } } 此代碼的結果輸出是多少?答案是20 40 60,前面兩個值,大家應該很容易就能看出來,但第三個值為什麼是60呢?先來看看程序的執行流程:Closures函數調用GetClosureFunc函數並進入其中。函數調用語句中帶了一個參數30。這是由於GetClosureFunc返回的是一個函數,即執行時再次調用了這個函數,進入GetClosureFunc函數中,首先val的值為10,通過internalAdd方法傳入一個值10,因此第一個輸出值為20,往下走,val的值變成30,通過internalAdd方法傳入值10,於是第二個輸出值為40。從這裡我們大致可以看出,局部函數和局部變量如何在同一個作用域中起作用,顯然,對局部變量的改變會影響internalAdd的值,盡管變量的改變發生在internalAdd最初的創建之後。最後,GetClosureFunc返回了internalAdd方法,以參數30再次調用這個函數,於是,結果成為60。 初看起來,這並不真正符合邏輯。val應該是一個局部變量,它生存在棧中,當GetClosureFunc函數返回時,它就不在了,不是麼?確實如此,這正是閉包的目的,當編譯器會明白無誤地警告這種情況會引起程序的崩潰時阻止變量值超出其作用域之外。 從技術角度來看,數據保存的位置很重要,編譯器創建一個匿名類,並在GetClosureFunc中創建這個類的實例——如果不需要閉包起作用,則那個匿名函數只會與GetClosureFunc生存在同一個類中,最後,局部變量val實際上不再是一個局部變量,而是匿名類中的一個字段。其結果是,internalAdd現在可以引用保存在匿名類實例中的函數。這個實例中也包含變量val的數據。只要保持internalAdd的引用,變量val的值就一直保存著。 下面這段代碼說明編譯器在這種情形下采用的模式: private sealed class DisplayClass { public int val; public int AnonymousFunc(int x) { return x + this.val; } private static Func<int, int> GetClosureFunc() { DisplayClass displayClass = new DisplayClass(); displayClass.val = 10; Func<int, int> internalAdd = displayClass.AnonymousFunc; Console.WriteLine(internalAdd(10)); displayClass.val = 30; Console.WriteLine(internalAdd(10)); return internalAdd; } } 回到動態創建函數思想:現在可以憑空創建新的函數,而且它的功能因參數而異。例如,下面這個函數把一個靜態值加到一個參數上: private static void DynamicAdd() { var add5 = GetAddX(5); var add10 = GetAddX(10); Console.WriteLine(add5(10)); Console.WriteLine(add10(10)); } private static Func<int,int> GetAddX(int staticVal) { return x => staticVal + x; } 這個原理正是許多函數構建技術的基礎,這種方法顯然與方法重載等面向對象方法相對應。但是與方法重載不同,匿名函數的創建可以在運行時動態發生,只需受另一個函數中的一行代碼觸發。為使某個算法更加容易讀和寫而使用的特殊函數可以在調用它的方法中創建,而不是再類級別上胡亂添加函數或方法——這正是函數模塊化的核心思想。