在Scala中,函數(shù)引入傳入的參數(shù)是再正常不過的事情了,比如
(x: Int) => x > 0
中畦浓,唯一在函數(shù)體x > 0
中用到的變量是x直奋,即這個函數(shù)的唯一參數(shù)。
除此之外九杂,Scala還支持引用其他地方定義的變量:
(x: Int) => x + more
颁湖,這個函數(shù)將more
也作為入?yún)ⅲ贿^這個參數(shù)是哪里來的尼酿?從這個函數(shù)的角度來看爷狈,more是一個自由變量,因為函數(shù)字面量本身并沒有給more賦予任何含義裳擎。相反涎永,x是一個綁定變量,因為它在該函數(shù)的上下文里有明確的定義:它被定義為該函數(shù)的唯一參數(shù)。如果單獨使用這個函數(shù)字面量羡微,而沒有在任何處于作用域內(nèi)的地方定義more谷饿,編譯器將報錯:
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
運行時從這個函數(shù)字面量創(chuàng)建出來的函數(shù)值(對象)被稱為閉包博投。該名稱源于“捕獲”其自由變量從而“閉合”該函數(shù)字面量的動作。沒有自由變量的函數(shù)字面量盯蝴,比如(x: Int) => x + 1
毅哗,稱為閉合語(這里的語指的是一段源代碼)。因此捧挺,運行時從這個函數(shù)字面量創(chuàng)建出來的函數(shù)值嚴(yán)格來說并不是一個閉包虑绵,因為(x: Int) => x + 1
按照目前這個寫法已經(jīng)是閉合的了。而運行時從任何帶有自由變量的函數(shù)字面量闽烙,比如(x: Int) => x + more
創(chuàng)建的函數(shù)翅睛,按照定義,要求捕獲到它的自由變量more的綁定黑竞。相應(yīng)的函數(shù)值結(jié)果(包含指向被捕獲的more變量的引用)就被稱為閉包捕发,因為函數(shù)值是通過閉合這個開放語的動作產(chǎn)生的。
這個例子帶來一個問題:如果more在閉包創(chuàng)建以后被改變會發(fā)生什么很魂?在Scala中扎酷,答案是閉包能夠看到這個改變,參考下面的例子:
scala> more = 9999
more: Int = 9999
scala> addMore(10)
res1: Int = 10009
很符合直覺的是莫换,Scala的閉包捕獲的是變量本身霞玄,而不是變量引用的值。正如前面示例所展示的拉岁,為(x: Int) => x + more
創(chuàng)建的閉包能夠看到閉包外對more的修改坷剧。反過來也是成立的:閉包對捕獲到的變量的修改也能在閉包外被看到。參考下面的例子:
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
這個例子通過遍歷的方式來對List中的數(shù)字求和喊暖。sum這個變量位于函數(shù)字面量sum += _
的外圍作用域惫企,這個函數(shù)將數(shù)字加給sum。雖然運行時是這個閉包對sum進行的修改陵叽,最終的結(jié)果-11仍然能被閉包外部看到狞尔。
那么,如果一個閉包訪問了某個隨著程序運行會產(chǎn)生多個副本的變量會如何呢巩掺?例如偏序,如果一個閉包使用了某個函數(shù)的局部變量,而這個函數(shù)又被調(diào)用了多次胖替,會怎么樣研儒?閉包每次訪問到的是這個變量的哪一個實例呢豫缨?
答案是:閉包引用的實例是在閉包被創(chuàng)建時活躍的那一個。參考下面的函數(shù)端朵,函數(shù)創(chuàng)建并返回more閉包的函數(shù)
def makeIncreaser(more: Int) = (x: Int) => x + more
該函數(shù)每調(diào)用一次好芭,就會創(chuàng)建一個新的閉包。每個閉包都會訪問那個在它創(chuàng)建時活躍的變量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)
時冲呢,一個捕獲了more的綁定值為1的閉包就被創(chuàng)建并返回舍败。同理,當(dāng)調(diào)用makeIncreaser(9999)
時敬拓,返回的是一個捕獲了more的綁定值9999的閉包邻薯。當(dāng)你將這些閉包應(yīng)用到入?yún)r,其返回結(jié)果取決于閉包創(chuàng)建時more的定義
scala> inc1(10)
res4: Int = 11
scala> inc9999(10)
res5: Int = 10009
這里恩尾,more是某次方法調(diào)用的入?yún)⒊谒担椒ㄒ呀?jīng)返回了挽懦,不過這并沒有影響翰意。Scala編譯器會重新組織和安排,讓被捕獲的參數(shù)在堆上繼續(xù)存活信柿。這樣的安排都是由編譯器自動完成的冀偶,使用者并不需要關(guān)心。