Scala中閉包

在Scala中,函數(shù)引入傳入的參數(shù)是再正常不過(guò)的事情了,比如
(x: Int) => x > 0中,唯一在函數(shù)體x > 0中用到的變量是x,即這個(gè)函數(shù)的唯一參數(shù)。

除此之外,Scala還支持引用其他地方定義的變量:
(x: Int) => x + more,這個(gè)函數(shù)將more也作為入?yún)ⅲ贿^(guò)這個(gè)參數(shù)是哪里來(lái)的?從這個(gè)函數(shù)的角度來(lái)看,more是一個(gè)自由變量,因?yàn)楹瘮?shù)字面量本身并沒(méi)有給more賦予任何含義。相反,x是一個(gè)綁定變量,因?yàn)樗谠摵瘮?shù)的上下文里有明確的定義:它被定義為該函數(shù)的唯一參數(shù)。如果單獨(dú)使用這個(gè)函數(shù)字面量,而沒(méi)有在任何處于作用域內(nèi)的地方定義more,編譯器將報(bào)錯(cuò):

scala> (x: Int) => x + more
<console>:12: error: not found: value more
       (x: Int) => x + more

另一方面,只要能找到名為more的變量,同樣的函數(shù)字面量就能正常工作:

scala> var more = 1
more: Int = 1

scala> val addMore = (x: Int) => x + more
addMore: Int => Int = $$Lambda$1104/583744857@33e4b9c4

scala> addMore(10)
res0: Int = 11

運(yùn)行時(shí)從這個(gè)函數(shù)字面量創(chuàng)建出來(lái)的函數(shù)值(對(duì)象)被稱(chēng)為閉包。該名稱(chēng)源于“捕獲”其自由變量從而“閉合”該函數(shù)字面量的動(dòng)作。沒(méi)有自由變量的函數(shù)字面量,比如(x: Int) => x + 1,稱(chēng)為閉合語(yǔ)(這里的語(yǔ)指的是一段源代碼)。因此,運(yùn)行時(shí)從這個(gè)函數(shù)字面量創(chuàng)建出來(lái)的函數(shù)值嚴(yán)格來(lái)說(shuō)并不是一個(gè)閉包,因?yàn)?code>(x: Int) => x + 1按照目前這個(gè)寫(xiě)法已經(jīng)是閉合的了。而運(yùn)行時(shí)從任何帶有自由變量的函數(shù)字面量,比如(x: Int) => x + more創(chuàng)建的函數(shù),按照定義,要求捕獲到它的自由變量more的綁定。相應(yīng)的函數(shù)值結(jié)果(包含指向被捕獲的more變量的引用)就被稱(chēng)為閉包,因?yàn)楹瘮?shù)值是通過(guò)閉合這個(gè)開(kāi)放語(yǔ)的動(dòng)作產(chǎn)生的。

這個(gè)例子帶來(lái)一個(gè)問(wèn)題:如果more在閉包創(chuàng)建以后被改變會(huì)發(fā)生什么?在Scala中,答案是閉包能夠看到這個(gè)改變,參考下面的例子:

scala> more = 9999
more: Int = 9999

scala> addMore(10)
res1: Int = 10009

很符合直覺(jué)的是,Scala的閉包捕獲的是變量本身,而不是變量引用的值。正如前面示例所展示的,為(x: Int) => x + more創(chuàng)建的閉包能夠看到閉包外對(duì)more的修改。反過(guò)來(lái)也是成立的:閉包對(duì)捕獲到的變量的修改也能在閉包外被看到。參考下面的例子:

scala> val someNumbers = List(-11, -10, -5, 0, 5, 10)
someNumbers: List[Int] = List(-11, -10, -5, 0, 5, 10)

scala> var sum = 0
sum: Int = 0

scala> someNumbers.foreach(sum += _)

scala> sum
res3: Int = -11

這個(gè)例子通過(guò)遍歷的方式來(lái)對(duì)List中的數(shù)字求和。sum這個(gè)變量位于函數(shù)字面量sum += _的外圍作用域,這個(gè)函數(shù)將數(shù)字加給sum。雖然運(yùn)行時(shí)是這個(gè)閉包對(duì)sum進(jìn)行的修改,最終的結(jié)果-11仍然能被閉包外部看到。

那么,如果一個(gè)閉包訪問(wèn)了某個(gè)隨著程序運(yùn)行會(huì)產(chǎn)生多個(gè)副本的變量會(huì)如何呢?例如,如果一個(gè)閉包使用了某個(gè)函數(shù)的局部變量,而這個(gè)函數(shù)又被調(diào)用了多次,會(huì)怎么樣?閉包每次訪問(wèn)到的是這個(gè)變量的哪一個(gè)實(shí)例呢?

答案是:閉包引用的實(shí)例是在閉包被創(chuàng)建時(shí)活躍的那一個(gè)。參考下面的函數(shù),函數(shù)創(chuàng)建并返回more閉包的函數(shù)

def makeIncreaser(more: Int) = (x: Int) => x + more

該函數(shù)每調(diào)用一次,就會(huì)創(chuàng)建一個(gè)新的閉包。每個(gè)閉包都會(huì)訪問(wèn)那個(gè)在它創(chuàng)建時(shí)活躍的變量more

scala> val inc1 = makeIncreaser(1)
inc1: Int => Int = $$Lambda$1269/1504482477@1179731c

scala> val inc9999 = makeIncreaser(9999)
inc9999: Int => Int = $$Lambda$1269/1504482477@2dba6013

當(dāng)調(diào)用makeIncreaser(1)時(shí),一個(gè)捕獲了more的綁定值為1的閉包就被創(chuàng)建并返回。同理,當(dāng)調(diào)用makeIncreaser(9999)時(shí),返回的是一個(gè)捕獲了more的綁定值9999的閉包。當(dāng)你將這些閉包應(yīng)用到入?yún)r(shí),其返回結(jié)果取決于閉包創(chuàng)建時(shí)more的定義

scala> inc1(10)
res4: Int = 11

scala> inc9999(10)
res5: Int = 10009

這里,more是某次方法調(diào)用的入?yún)?,而方法已?jīng)返回了,不過(guò)這并沒(méi)有影響。Scala編譯器會(huì)重新組織和安排,讓被捕獲的參數(shù)在堆上繼續(xù)存活。這樣的安排都是由編譯器自動(dòng)完成的,使用者并不需要關(guān)心。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容