閉包(Closure)


閉包(Closure)是擁有閒置變數(Free variable)的運算式。閒置變數真正扮演的角色依當時語彙環境(Lexical environment)而定。支援閉包的程式語言通常具有一級函式(First-class function)。建立函式不等於建立閉包。如果函式的閒置變數與當時語彙環境綁定,該函式才稱為閉包。

那麼何為閒置變數?閒置變數是指對於函式而言,既非區域變數也非參數的變數,像區域變數或參數,其作用範圍基本上在被定義的函式範 圍中。它是被綁定變數(Bound variable)。舉個例子來說:
// 對 doSome 來說,x 是 bound variable
def doSome() = {
val x = 10
// 對 f 來說,x 是 free variable,y 是 bound variable
 val f = (y: Int) => x + y
f
}

上面doSome的例子中,(y: Int) => x + y 形成了一個閉包,如果你單看:
val f = (y: Int) => x + y

看來起x似乎沒有定義。實際上,x 是從外部函式捕捉而來。閉包是個捕捉了外部函式變數(或使之繼續存活)的函式。在上面doSome的例子中,(y: Int) => x + y 形成了一個閉包,因為它將變數 x 關入(close)自己的範圍。如果形式閉包的函式物件持續存活,被關閉的變數 x 也會繼續存活。就像是延續了變數 x 的生命週期。

舉個例子來說:
val foo = doSome()
println(foo(20)) // 顯示 30
println(foo(30)) // 顯示 40

由 於doSome函式傳回了函式物件,上例中將傳回的函式物件指定給foo,就doSome而言已經執行完畢,單看x的話,理應x已結束其生命週期,但由於 doSome中建立了閉包並傳回,x被關閉在閉包中,所以x的生命週期就與閉包的生命週期相同了,如上例所示,呼叫foo(20)結果就是10+20(因 為被閉關的x值是10),呼叫foo(30)結果就是10+30。

注意!閉包關閉的是變數,而不是變數所參考的值。下面這個範例可以證明:
def doOther() = {
var x = 10
val f = x + (_ : Int) // 佔位字元語法
x = 100
f
}

val foo = doOther()
println(foo(20)) // 顯示 120
println(foo(30)) // 顯示 130

在建立閉包時,綁定了x變數,而不是數值10(x變數的值),也因此doOther之後改變了x變數的值,而後傳回閉包給foo參數後,範例顯示的結果分別是100+20與100+30。由於閉包綁定的是變數,所以你也可以在閉包中改變變數的值:
var sum = 0
val arr = Array(1, 2, 3, 4, 5)
arr.foreach(sum += _)
println(sum) // 顯示 15

你可能會有疑問的是,如果閉包關閉了某個變數,使得該變數的生命週期得以延長,那麼這個會怎麼樣?
def doOther() = {
var x = 10
val f = () => { x -= 1; x }
f
}

val f1 = doOther()
val f2 = doOther()

println(f1()) // 顯示 9
println(f2()) // 顯示?

在這個範例中,doOther被呼叫了兩次(或更多次),doOther中的閉包關閉了x,並對其執行了遞減。呼叫了f1時,x會被遞減1,所以顯示9,這沒有問題,那麼呼叫f2()後,結果是多少?

像這類的例子,其實結果是很一致的,關閉的是建立閉包時外部範圍下的變數。以上例來說,第一次呼叫doOther時,建立了x變數,指定值給x變數,而後建立閉包將之關閉。第二次呼叫doOther時,建立了x變數,指定值給x變數,而後建立閉包將之關閉。所以f1與f2關閉的根本是不同作用範圍的x變數(也就是該次呼叫doOther時所建立的x變數)。所以上例中,呼叫f2之後顯示的值仍是9。

下面這個也是個類似的例子:
def doSome(x: Int) = (a: Int) => x + a

val f1 = doSome(100) // 閉包綁定的是該次呼叫時所建立的x參數
val f2 = doSome(200) // 閉包綁定的是該次呼叫時所建立的x參數
println(f1(10)) // 顯示 110
println(f2(10)) // 顯示 210

雖然沒有指出,不過上一個主題 一級函式(First-class function) 的最後一個範例,已經應用了閉包來解決問題:
def toOneByRow(array: Array[Array[Int]]) = {
toOne(array, _ * array(0).length + _)
}

def toOneByColumn(array: Array[Array[Int]]) = {
toOne(array, _ + _ * array.length)
}

def toOne(array: Array[Array[Int]], index: (Int, Int) => Int) = {
val arr = new Array[Int](array.length * array(0).length)
for(row <- 0 until array.length; column <- 0 until array(0).length) {
arr(index(row, column)) = array(row)(column)
}
arr
}