本文介紹了Groovy閉包的有關(guān)內(nèi)容。閉包可以說是Groovy中最重要的功能了。如果沒有閉包,那么Groovy除了語法比Java簡單點之外,沒有任何優(yōu)勢。但是閉包,讓Groovy這門語言具有了強大的功能。如果你希望構(gòu)建自己的領(lǐng)域描述語言(DSL),Groovy是一個很好的選擇。Gradle就是一個非常成功的例子。
本文參考自Groovy 文檔 閉包,為了方便,大部分代碼直接引用了Groovy文檔。
定義閉包
閉包在花括號內(nèi)定義。我們可以看到Groovy閉包和Java的lambda表達式差不多,但是學習之后就會發(fā)現(xiàn),Groovy的閉包功能更加強大。
{ [closureParameters -> ] statements }
閉包的參數(shù)列表是可選的,參數(shù)的類型也是可選的。如果我們不指定參數(shù)的類型,會由編譯器自動推斷。如果閉包只有一個參數(shù),這個參數(shù)可以省略,我們可以直接使用it來訪問該參數(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 } //顯示指定閉包沒有參數(shù)
閉包的參數(shù)還可以使用可變參數(shù)。
def concat1 = { String... args -> args.join('') } //可變參數(shù),個數(shù)不定
使用閉包
我們可以將閉包賦給變量,然后可以將變量作為函數(shù)來調(diào)用,或者調(diào)用閉包的call方法也可以調(diào)用閉包。閉包實際上是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)對象
Groovy的閉包比Java的Lambda表達式功能更強大。原因就是Groovy閉包可以修改委托對象和委托策略。這樣Groovy就可以實現(xiàn)非常優(yōu)美的領(lǐng)域描述語言(DSL)了。Gradle就是一個鮮明的例子。
Groovy閉包有3個相關(guān)對象。
- this 即閉包定義所在的類。
- owner 即閉包定義所在的對象或閉包。
- delegate 即閉包中引用的第三方對象。
前面兩個對象都很好理解。delegate對象需要我們手動指定。
class Person {
String name
}
def p = new Person(name:'Igor')
def cl = { name.toUpperCase() }
cl.delegate = p
assert cl() == 'IGOR'
相應的Groovy有幾種屬性解析策略,幫助我們解析閉包中遇到的屬性和方法引用。我們可以使用閉包的resolveStrategy屬性修改策略。
-
Closure.OWNER_FIRST,默認策略,首先從owner上尋找屬性或方法,找不到則在delegate上尋找。 -
Closure.DELEGATE_FIRST,和上面相反。 -
Closure.OWNER_ONLY,只在owner上尋找,delegate被忽略。 -
Closure.DELEGATE_ONLY,和上面相反。 -
Closure.TO_SELF,高級選項,讓開發(fā)者自定義策略。
Groovy文檔有詳細的代碼例子,說明了這幾種策略的行為。這里就不再細述了。
函數(shù)式編程
GString的閉包
先看下面的例子。我們使用了GString的內(nèi)插字符串,將一個變量插入到字符串中。這工作非常正常。
def x = 1
def gs = "x = ${x}"
assert gs == 'x = 1'
如果我們現(xiàn)在改變了變量的值,然后再看看結(jié)果。結(jié)果可能出乎你的意料,輸出仍然是x = 1。原因有兩個:一是GString只能延遲計算值的toString表示形式;二是表達式${x}的計算發(fā)生在GString創(chuàng)建的時候,然后就不會計算了。
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ù)范例
柯里化
首先來看看閉包的柯里化,也就是將多個參數(shù)的函數(shù)轉(zhuǎn)變?yōu)橹唤邮芤粋€參數(shù)的函數(shù)。我們在閉包上調(diào)用ncurry方法來實現(xiàn),它會固定指定索引的參數(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ù)列做例子。這個實現(xiàn)的缺點就是重復計算次數(shù)太多了。Groovy文檔給出的評價是naive!
def fib
fib = { long n -> n<2?n:fib(n-1)+fib(n-2) }
assert fib(15) == 610 // 太慢了
我們可以在閉包上調(diào)用memoize()方法來生成一個新閉包,該閉包具有緩存執(zhí)行結(jié)果的行為。緩存使用近期最少使用算法(LRU)。
fib = { long n -> n<2?n:fib(n-1)+fib(n-2) }.memoize()
assert fib(25) == 75025 //很快
緩存會使用閉包的實際參數(shù)的值,因此我們在使用非基本類型參數(shù)的時候必須格外小心,避免構(gòu)造大量對象或者進行無謂的裝箱、拆箱操作。
還有幾個方法提供了不同的緩存行為。
-
memoizeAtMost生成一個最多緩存N個對象的新閉包。 -
memoizeAtLeast生成一個最少緩存N個對象的新閉包。 -
memoizeBetween生成一個新閉包,緩存?zhèn)€數(shù)在給定的兩者之間。
復合
閉包還可以復合。學過高數(shù)的話應該很好理解,這就是多個函數(shù)的復合(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,可惜我沒明白是什么意思。不過這里的意思就是尾遞歸,所以我就這么叫了。遞歸函數(shù)在調(diào)用層數(shù)過多的時候,有可能會用盡??臻g,導致拋出StackOverflowException。我們可以使用閉包的尾遞歸來避免爆棧。
普通的遞歸函數(shù),需要在自身中調(diào)用自身,因此必須有多層函數(shù)調(diào)用棧。如果遞歸函數(shù)的最后一個語句是遞歸調(diào)用本身,那么就有可能執(zhí)行尾遞歸優(yōu)化,將多層函數(shù)調(diào)用轉(zhuǎn)化為連續(xù)的函數(shù)調(diào)用。這樣函數(shù)調(diào)用棧只有一層,就不會發(fā)生爆棧異常了。
尾遞歸需要調(diào)用閉包的trampoline()方法,它會返回一個TrampolineClosure,具有尾遞歸特性。注意這里我們需要將外層閉包和遞歸閉包都調(diào)用trampoline()方法,才能正確的使用尾遞歸特性。然后我們計算一個很大的數(shù)字,就不會出現(xiàn)爆棧錯誤了。
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