本文介紹了Groovy閉包的有關(guān)內(nèi)容。閉包可以說(shuō)是Groovy中最重要的功能了沐绒。如果沒(méi)有閉包俩莽,那么Groovy除了語(yǔ)法比Java簡(jiǎn)單點(diǎn)之外,沒(méi)有任何優(yōu)勢(shì)乔遮。但是閉包扮超,讓Groovy這門語(yǔ)言具有了強(qiáng)大的功能。如果你希望構(gòu)建自己的領(lǐng)域描述語(yǔ)言(DSL)蹋肮,Groovy是一個(gè)很好的選擇出刷。Gradle就是一個(gè)非常成功的例子。
本文參考自Groovy 文檔 閉包坯辩,為了方便馁龟,大部分代碼直接引用了Groovy文檔。
定義閉包
閉包在花括號(hào)內(nèi)定義漆魔。我們可以看到Groovy閉包和Java的lambda表達(dá)式差不多坷檩,但是學(xué)習(xí)之后就會(huì)發(fā)現(xiàn),Groovy的閉包功能更加強(qiáng)大改抡。
{ [closureParameters -> ] statements }
閉包的參數(shù)列表是可選的矢炼,參數(shù)的類型也是可選的。如果我們不指定參數(shù)的類型阿纤,會(huì)由編譯器自動(dòng)推斷句灌。如果閉包只有一個(gè)參數(shù),這個(gè)參數(shù)可以省略欠拾,我們可以直接使用it
來(lái)訪問(wèn)該參數(shù)胰锌。以下是Groovy文檔的例子。下面這些都是合法的閉包藐窄。
{ item++ }
{ -> item++ }
{ println it }
{ it -> println it }
{ name -> println name }
{ String x, int y ->
println "hey ${x} the value is ${y}"
}
{ reader ->
def line = reader.readLine()
line.trim()
}
需要注意閉包的隱式參數(shù)it
總是存在资昧,即使我們省去->
操作符。除非我們顯式在閉包的參數(shù)列表上什么都不指定枷邪。
def magicNumber = { -> 42 } //顯示指定閉包沒(méi)有參數(shù)
閉包的參數(shù)還可以使用可變參數(shù)榛搔。
def concat1 = { String... args -> args.join('') } //可變參數(shù)诺凡,個(gè)數(shù)不定
使用閉包
我們可以將閉包賦給變量东揣,然后可以將變量作為函數(shù)來(lái)調(diào)用,或者調(diào)用閉包的call方法也可以調(diào)用閉包腹泌。閉包實(shí)際上是groovy.lang.Closure
類型嘶卧,泛型版本的泛型表示閉包的返回類型。
def fun = { println("$it") }
fun(1234)
Closure date = { println(LocalDate.now()) }
date.call()
Closure<LocalTime> time = { LocalTime.now() }
println("now is ${time()}")
委托策略
閉包的相關(guān)對(duì)象
Groovy的閉包比Java的Lambda表達(dá)式功能更強(qiáng)大凉袱。原因就是Groovy閉包可以修改委托對(duì)象和委托策略芥吟。這樣Groovy就可以實(shí)現(xiàn)非常優(yōu)美的領(lǐng)域描述語(yǔ)言(DSL)了侦铜。Gradle就是一個(gè)鮮明的例子。
Groovy閉包有3個(gè)相關(guān)對(duì)象钟鸵。
- this 即閉包定義所在的類钉稍。
- owner 即閉包定義所在的對(duì)象或閉包。
- delegate 即閉包中引用的第三方對(duì)象棺耍。
前面兩個(gè)對(duì)象都很好理解贡未。delegate對(duì)象需要我們手動(dòng)指定。
class Person {
String name
}
def p = new Person(name:'Igor')
def cl = { name.toUpperCase() }
cl.delegate = p
assert cl() == 'IGOR'
相應(yīng)的Groovy有幾種屬性解析策略蒙袍,幫助我們解析閉包中遇到的屬性和方法引用俊卤。我們可以使用閉包的resolveStrategy
屬性修改策略。
-
Closure.OWNER_FIRST
害幅,默認(rèn)策略消恍,首先從owner上尋找屬性或方法,找不到則在delegate上尋找以现。 -
Closure.DELEGATE_FIRST
狠怨,和上面相反。 -
Closure.OWNER_ONLY
邑遏,只在owner上尋找取董,delegate被忽略。 -
Closure.DELEGATE_ONLY
无宿,和上面相反茵汰。 -
Closure.TO_SELF
,高級(jí)選項(xiàng)孽鸡,讓開(kāi)發(fā)者自定義策略蹂午。
Groovy文檔有詳細(xì)的代碼例子,說(shuō)明了這幾種策略的行為彬碱。這里就不再細(xì)述了豆胸。
函數(shù)式編程
GString的閉包
先看下面的例子。我們使用了GString的內(nèi)插字符串巷疼,將一個(gè)變量插入到字符串中晚胡。這工作非常正常。
def x = 1
def gs = "x = ${x}"
assert gs == 'x = 1'
如果我們現(xiàn)在改變了變量的值嚼沿,然后再看看結(jié)果估盘。結(jié)果可能出乎你的意料,輸出仍然是x = 1
骡尽。原因有兩個(gè):一是GString只能延遲計(jì)算值的toString表示形式遣妥;二是表達(dá)式${x}
的計(jì)算發(fā)生在GString創(chuàng)建的時(shí)候,然后就不會(huì)計(jì)算了攀细。
x = 2
assert !gs == 'x = 2'
如果我們希望字符串的結(jié)果隨著變量的改變而改變箫踩,需要將${x}
聲明為閉包爱态。這樣,GString的行為就和我們想的一樣了境钟。
def x = 1
def gs = "x = ${-> x}"
assert gs == 'x = 1'
x = 2
assert gs == 'x = 2'
函數(shù)范例
柯里化
首先來(lái)看看閉包的柯里化锦担,也就是將多個(gè)參數(shù)的函數(shù)轉(zhuǎn)變?yōu)橹唤邮芤粋€(gè)參數(shù)的函數(shù)。我們?cè)陂]包上調(diào)用ncurry
方法來(lái)實(shí)現(xiàn)慨削,它會(huì)固定指定索引的參數(shù)吆豹。另外還有curry
和rcurry
方法,用于固定最左邊和最右邊的參數(shù)理盆。
def volume = { double l, double w, double h -> l*w*h }
def fixedWidthVolume = volume.ncurry(1, 2d) //將索引為1的參數(shù)固定為2d
assert volume(3d, 2d, 4d) == fixedWidthVolume(3d, 4d)
def fixedWidthAndHeight = volume.ncurry(1, 2d, 4d) //將寬和高固定
assert volume(3d, 2d, 4d) == fixedWidthAndHeight(3d)
緩存
我們還可以緩存閉包的結(jié)果痘煤。Groovy文檔用了斐波那契數(shù)列做例子。這個(gè)實(shí)現(xiàn)的缺點(diǎn)就是重復(fù)計(jì)算次數(shù)太多了猿规。Groovy文檔給出的評(píng)價(jià)是naive!
def fib
fib = { long n -> n<2?n:fib(n-1)+fib(n-2) }
assert fib(15) == 610 // 太慢了
我們可以在閉包上調(diào)用memoize()
方法來(lái)生成一個(gè)新閉包衷快,該閉包具有緩存執(zhí)行結(jié)果的行為。緩存使用近期最少使用算法(LRU)姨俩。
fib = { long n -> n<2?n:fib(n-1)+fib(n-2) }.memoize()
assert fib(25) == 75025 //很快
緩存會(huì)使用閉包的實(shí)際參數(shù)的值蘸拔,因此我們?cè)谑褂梅腔绢愋蛥?shù)的時(shí)候必須格外小心,避免構(gòu)造大量對(duì)象或者進(jìn)行無(wú)謂的裝箱环葵、拆箱操作调窍。
還有幾個(gè)方法提供了不同的緩存行為。
-
memoizeAtMost
生成一個(gè)最多緩存N個(gè)對(duì)象的新閉包张遭。 -
memoizeAtLeast
生成一個(gè)最少緩存N個(gè)對(duì)象的新閉包邓萨。 -
memoizeBetween
生成一個(gè)新閉包,緩存?zhèn)€數(shù)在給定的兩者之間菊卷。
復(fù)合
閉包還可以復(fù)合缔恳。學(xué)過(guò)高數(shù)的話應(yīng)該很好理解,這就是多個(gè)函數(shù)的復(fù)合(f(g(x))和g(f(x))的區(qū)別)洁闰。
def plus2 = { it + 2 }
def times3 = { it * 3 }
def times3plus2 = plus2 << times3
assert times3plus2(3) == 11
assert times3plus2(4) == plus2(times3(4))
def plus2times3 = times3 << plus2
assert plus2times3(3) == 15
assert plus2times3(5) == times3(plus2(5))
// reverse composition
assert times3plus2(3) == (times3 >> plus2)(3)
尾遞歸(Trampoline)
文檔原文是Trampoline歉甚,可惜我沒(méi)明白是什么意思。不過(guò)這里的意思就是尾遞歸扑眉,所以我就這么叫了纸泄。遞歸函數(shù)在調(diào)用層數(shù)過(guò)多的時(shí)候,有可能會(huì)用盡椦兀空間聘裁,導(dǎo)致拋出StackOverflowException
。我們可以使用閉包的尾遞歸來(lái)避免爆棧耸弄。
普通的遞歸函數(shù)咧虎,需要在自身中調(diào)用自身,因此必須有多層函數(shù)調(diào)用棧计呈。如果遞歸函數(shù)的最后一個(gè)語(yǔ)句是遞歸調(diào)用本身砰诵,那么就有可能執(zhí)行尾遞歸優(yōu)化,將多層函數(shù)調(diào)用轉(zhuǎn)化為連續(xù)的函數(shù)調(diào)用捌显。這樣函數(shù)調(diào)用棧只有一層茁彭,就不會(huì)發(fā)生爆棧異常了。
尾遞歸需要調(diào)用閉包的trampoline()
方法扶歪,它會(huì)返回一個(gè)TrampolineClosure
理肺,具有尾遞歸特性。注意這里我們需要將外層閉包和遞歸閉包都調(diào)用trampoline()
方法善镰,才能正確的使用尾遞歸特性妹萨。然后我們計(jì)算一個(gè)很大的數(shù)字,就不會(huì)出現(xiàn)爆棧錯(cuò)誤了炫欺。
def factorial
factorial = { int n, def accu = 1G ->
if (n < 2) return accu
factorial.trampoline(n - 1, n * accu)
}
factorial = factorial.trampoline()
assert factorial(1) == 1
assert factorial(3) == 1 * 2 * 3
assert factorial(1000) // == 402387260.. plus another 2560 digits