1. 程式人生 > >程式語言的閉包

程式語言的閉包

閉包的概念、形式與應用

什麼是閉包?

閉包並不是什麼新奇的概念,它早在高階語言開始發展的年代就產生了。閉包(Closure)是詞法閉包(Lexical Closure)的簡稱。閉包是由函式及其相關的引用環境組合而成的實體(即:閉包=函式+引用環境)。

函式只是一段可執行程式碼,編譯後就"固化"了,每個函式在記憶體中只有一份例項,得到函式的入口便可以執行函式。在函數語言程式設計語言中,函式是一階值,函式可以作為另一個函式的引數或返回值,可以賦給一個變數。函式可以欠巢狀定義,即在一個函式內部可以定義另一個函式,有了巢狀函式這種結構,便會產生閉包問題。如:

def ExFunc(n):

    sum=n

    def InsFunc():

        return sum+1

    return InsFunc

 

>>myFunc=ExFunc(10)

>>myFunc()

11

>>myAnotherFunc=ExFunc(20)

>>myAnotherFunc()

21

>>myFunc()

11

>>myAnotherFunc()

21

在這段程式中,函式InsFunc是函式ExFunc的內嵌函式,並且是ExFunc函式的返回值。我們注意到一個問題:內嵌函式InFunc中引用到外層函式中的區域性變數sum, Python會怎麼處理這個問題呢?先讓我們來看看這段程式碼的執行結果。當我們呼叫分別由不同的引數呼叫ExFunc函式得到的函式時(myFunc(),myAnotherFunc()),得到的結果是隔離的,也就是說每次呼叫ExFunc函式後都將生成並儲存一個新的區域性比變數sum。其實這裡ExFunc函式返回的就是閉包。

引用環境

按照命令式語言的規則,ExFunc函式只是返回了內嵌函式InsFunc的地址,在執行InsFunc函式時將會由於在其作用域內找到不到sum變數而出錯。而在函式式語言中,當內嵌函式體內引用到體外的變數時,將會把定義時涉及到的引用環境和函式體打包成一個整體(閉包)返回。現在給出引用環境的定義就容易理解了:應用環境是指在程式執行中的某個點所有處於活躍狀態的約束(一個變數的名字和其所代表的物件之間的聯絡)所組成的集合。閉包的使用和正常的函式呼叫沒有區別。

由於閉包把函式和執行時的引用環境打包成為一個新的整體,所以就解決了函式程式設計中的巢狀問題所引發的問題。如上述程式碼段中,當每次呼叫ExFunc函式時都將返回一個新的閉包例項,這些例項之間是隔離的,分別包含呼叫時不同的引用環境現場。不同於函式,閉包的執行時可以有多個例項,不同的引用環境和相同的函式組合可以產生不同的例項。

 

 

閉包只是在形式和表現上像函式,但實際上不是函式。函式是一些可執行程式碼,這些程式碼在函式被定以後就確定了,不會在執行時發生變化,所以一個函式只有一個例項。閉包在執行時可以有多個例項,不同的引用環境和相同的函式組合可以產生不同的例項。所謂引用環境是指在程式執行中的某個點所有處於活躍狀態的約束所組成的集合。其中的約束是指一個變數的名字和其所代表的物件之間的聯絡。那麼為什麼要引用環境與函式組合起來呢?這主要是因為在支援巢狀作用域的語言中,有時不能簡單直接地確定函式的引用環境。這樣的語言一般具有這樣的特性:

  • 函式是一階值(First-class value),即函式可以作為另一個函式的返回值或引數,還可以作為一個變數的值。
  • 函式可以巢狀定義,即在一個函式內部可以定義另一個函式。

考慮如下程式碼

function make_counter()

    local count=0

    function inc_count()

        count=count+1

        return count

    end

    return inc_count

end

c1=make_counter()

c2=make_counter()

print(c1())

print(c2())

在這段程式中,函式inc_count定義在函式make_counter內部,並作為make_counter的返回值。變數count不是inc_count內的區域性變數,按照最內巢狀作用域的規則,inc_count中的count引用的是最外層函式中的區域性變數count。接下來的程式碼中兩次呼叫make_counter()

這裡存在一個問題,當呼叫make_counter時,在其執行上下文中生成了區域性變數cout的例項,所以函式inc_count中的count引用的就是這個例項。但是inc_count並沒有在此時被執行,而是作為返回值返回。當make_counter返回後,其執行上下文失效,count例項的生命週期也就結束了,在後面對c1和c2呼叫實際是對inc_count的呼叫,而此處並不在count的作用域中,這看起來是無法正確執行的。

在這樣的語言中,如果按照作用域規則在執行時確定一個函式的引用環境,那麼這個引用環境可能和函式定義時不同。要想使這兩段程式正常執行,一個簡單的辦法是在函式定義時捕獲當時的引用環境,並與函式程式碼組合成一個整體。當把這個整個當做函式呼叫時,先把其中的引用環境覆蓋到當前的引用環境上,然後執行具體程式碼,並在呼叫結束後恢復原來的引用環境。這樣就保證了函式定義和執行時的引用環境是相同的。這種由環境與函式程式碼組成的實體就是閉包。當然如果編譯器或直譯器能夠確定一個函式在定義和執行時的引用環境是相同的(一個函式中沒有自由變數時,引用環境不會發生變化),那就沒有必要把引用環境和程式碼組合起來了,這時只需要傳遞普通的函式就可以了。現在可以得出這樣的結論:閉包不是函式,只是行為和函式相似,不是所有被傳遞的函式都需要轉化為閉包,只有引用環境可能發生變化的函式才需要這樣做。

一個程式語言需要哪些特性來支援閉包呢,下面列出一些比較重要的條件:

函式是一階值

函式是可以巢狀定義

可以捕獲引用環境,並把引用環境和函式程式碼組成一個可呼叫的實體

允許定義匿名函式

這些不是必要條件,但具備這些條件能說明一個程式設計對閉包的支援較為完善。

借用一個非常好的說法來做個總結:物件是附有行為的資料,而閉包是附有資料的行為。

閉包的表現形式

Python中的閉包

Python因其簡單易學,功能強大而擁有很多擁護者,很多企業和組織在使用這種語言。Python使用縮排來區分作用域的做法也十分有特點。下面是一個python的例子:

def addx(x):

    def adder(y): return x+y

    return adder

add8=addx(8)

add9=addx(9)

print add8(100)

print add9(100)

在python中使用def來定義函式時,是必須有名字的,要想使用匿名函式,則需要使用lambda語句,向下面的程式碼這樣:

def addx(x):

    return lambda y: x+y

add8=addx(8)

add9=addx(9)

print add8(100)

print add9(100)

閉包的應用

閉包可以用優雅的方式來處理一些棘手的問題,有些程式設計師聲稱沒有閉包簡直就活不下去了。這雖然有些誇張,卻從側面說明閉包有著強大的功能。

加強模組化

閉包有利於模組化程式設計,它能以簡單的方式開發較小的模組,從而提高開發速度和程式的可複用性。和沒有使用閉包的程式相比,使用閉包可將模組劃分得更小。比如我們要計算一個數組中所有數字的和,這隻需要迴圈遍歷陣列,把遍歷到的數字加起來就行了。如果現在要計算所有元素的積呢?要列印所有的元素呢?解決這些問題都要對陣列進行遍歷,如果是在不支援閉包的語言中,我們不得不一次又一次重複寫迴圈語句。而在支援閉包的語言中識是不必要的。

抽象

閉包是資料和行為的組合,這使得閉包具有較好抽象能力。