目前為止關(guān)于函數(shù)式編程各種功能的討論都只局限在“純”函數(shù)式語言范圍內(nèi):這些語言都是lambda演算的實現(xiàn)并且都沒有那些和阿隆佐形式系統(tǒng)相沖突的特性。然而,很多函數(shù)式語言的特性哪怕是在lambda演算框架之外都是很有用的。確實,如果一個公理系統(tǒng)的實現(xiàn)可以用數(shù)學思維來看待程序,那么這個實現(xiàn)還是很有用的,但這樣的實現(xiàn)卻不一定可以付諸實踐。很多現(xiàn)實中的語言都選擇吸收函數(shù)式編程的一些元素,卻又不完全受限于函數(shù)式教條的束縛。很多這樣的語言(比如Common Lisp)都不要求所有的變量必須為final,可以修改他們的值。也不要求函數(shù)只能依賴于它們的參數(shù),而是可以讀寫函數(shù)外部的狀態(tài)。同時這些語言又包含了FP的特性,如高階函數(shù)。與在lambda演算限制下將函數(shù)作為參數(shù)傳遞不同,在指令式語言中要做到同樣的事情需要支持一個有趣的特性,人們常把它稱為lexical closure。還是來看看例子。要注意的是,這個例子中變量不是final,而且函數(shù)也可以讀寫其外部的變量:
Function makePowerFn(int power) {
int powerFn(int base) {
return pow(base, power);
}
return powerFn;
}
Function square = makePowerFn(2);
square(3); // returns 9
makePowerFn函數(shù)返回另一個函數(shù),這個新的函數(shù)需要一個整數(shù)參數(shù)然后返回它的平方值。執(zhí)行square(3)的時候具體發(fā)生了什么事呢?變量power并不在powerFn的域內(nèi),因為makePowerFn早就運行結(jié)束返回了,所以它的棧也已經(jīng)不存在了。那么square又是怎么正常工作的呢?這個時候需要語言通過某種方式支持繼續(xù)存儲power的值,以便square后面繼續(xù)使用。那么如果再定義一個函數(shù),cube,用來計算立方,又應(yīng)該怎么做呢?那么運行中的程序就必須存儲兩份power的值,提供給makePowerFn生成的兩個函數(shù)分別使用。這種保存變量值的方法就叫做closure。closure不僅僅保存宿主函數(shù)的參數(shù)值,還可以用在下例的用法中:
Function makeIncrementer() {
int n = 0;
int increment() {
return ++n;
}
}
Function inc1 = makeIncrementer();
Function inc2 = makeIncrementer();
inc1(); // returns 1;
inc1(); // returns 2;
inc1(); // returns 3;
inc2(); // returns 1;
inc2(); // returns 2;
inc2(); // returns 3;
運行中的程序負責存儲n的值,以便incrementer稍后可以訪問它。與此同時,程序還會保存多份n的拷貝,雖然這些值應(yīng)該在makeIncrementer返回后就消失,但在這個情況下卻繼續(xù)保留下來給每一個incrementer對象使用。這樣的代碼編譯之后會是什么樣子?closure幕后的真正工作機理又是什么?這次運氣不錯,我們有一個后臺通行證,可以一窺究竟。
一點小常識往往可以幫大忙。乍一看這些本地變量已經(jīng)不再受限于基本的域限制并擁有無限的生命周期了。于是可以得出一個很明顯的結(jié)論:它們已經(jīng)不是存在棧上,而是堆上了8。這么說來closure的實現(xiàn)和前面討論過的函數(shù)差不多,只不過closure多了一個額外的引用指向其外部的變量而已:
class some_function_t {
SymbolTable parentScope;
// ...
}
當closure需要訪問不在它本地域的變量時,就可以通過這個引用到更外一層的父域中尋找該變量。謎底揭開了!closure將函數(shù)編程與面向?qū)ο蟮姆椒ńY(jié)合了起來。下一次為了保存并傳遞某些狀態(tài)而創(chuàng)建類的時候,想想closure。它能在運行時從相應(yīng)的域中獲得變量,從而可以把該變量當初“成員變量”來訪問,也因為這樣,就不再需要去創(chuàng)建一個成員變量了。
更多建議: