Groovy 閉包
本文介紹了Groovy閉包的有關內容。閉包可以說是Groovy中最重要的功能了。如果沒有閉包,那麼Groovy除了語法比Java簡單點之外,沒有任何優勢。但是閉包,讓Groovy這門語言具有了強大的功能。如果你希望構建自己的領域描述語言(DSL),Groovy是一個很好的選擇。Gradle就是一個非常成功的例子。
本文參考自Groovy 文件 閉包,為了方便,大部分程式碼直接引用了Groovy文件。
定義閉包
閉包在花括號內定義。我們可以看到Groovy閉包和Java的lambda表示式差不多,但是學習之後就會發現,Groovy的閉包功能更加強大。
{ [closureParameters -> ] statements }
閉包的引數列表是可選的,引數的型別也是可選的。如果我們不指定引數的型別,會由編譯器自動推斷。如果閉包只有一個引數,這個引數可以省略,我們可以直接使用it
來訪問該引數。以下是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()
}
需要注意閉包的隱式引數it
->
操作符。除非我們顯式在閉包的引數列表上什麼都不指定。
def magicNumber = { -> 42 } //顯示指定閉包沒有引數
閉包的引數還可以使用可變引數。
def concat1 = { String... args -> args.join('') } //可變引數,個數不定
使用閉包
我們可以將閉包賦給變數,然後可以將變數作為函式來呼叫,或者呼叫閉包的call方法也可以呼叫閉包。閉包實際上是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()}")
委託策略
閉包的相關物件
Groovy的閉包比Java的Lambda表示式功能更強大。原因就是Groovy閉包可以修改委託物件和委託策略。這樣Groovy就可以實現非常優美的領域描述語言(DSL)了。Gradle就是一個鮮明的例子。
Groovy閉包有3個相關物件。
- 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
,高階選項,讓開發者自定義策略。
Groovy文件有詳細的程式碼例子,說明了這幾種策略的行為。這裡就不再細述了。
函數語言程式設計
GString的閉包
先看下面的例子。我們使用了GString的內插字串,將一個變數插入到字串中。這工作非常正常。
def x = 1
def gs = "x = ${x}"
assert gs == 'x = 1'
如果我們現在改變了變數的值,然後再看看結果。結果可能出乎你的意料,輸出仍然是x = 1
。原因有兩個:一是GString只能延遲計算值的toString表示形式;二是表示式${x}
的計算髮生在GString建立的時候,然後就不會計算了。
x = 2
assert !gs == 'x = 2'
如果我們希望字串的結果隨著變數的改變而改變,需要將${x}
宣告為閉包。這樣,GString的行為就和我們想的一樣了。
def x = 1
def gs = "x = ${-> x}"
assert gs == 'x = 1'
x = 2
assert gs == 'x = 2'
函式範例
柯里化
首先來看看閉包的柯里化,也就是將多個引數的函式轉變為只接受一個引數的函式。我們在閉包上呼叫ncurry
方法來實現,它會固定指定索引的引數。另外還有curry
和rcurry
方法,用於固定最左邊和最右邊的引數。
def volume = { double l, double w, double h -> l*w*h }
def fixedWidthVolume = volume.ncurry(1, 2d) //將索引為1的引數固定為2d
assert volume(3d, 2d, 4d) == fixedWidthVolume(3d, 4d)
def fixedWidthAndHeight = volume.ncurry(1, 2d, 4d) //將寬和高固定
assert volume(3d, 2d, 4d) == fixedWidthAndHeight(3d)
快取
我們還可以快取閉包的結果。Groovy文件用了斐波那契數列做例子。這個實現的缺點就是重複計算次數太多了。Groovy文件給出的評價是naive!
def fib
fib = { long n -> n<2?n:fib(n-1)+fib(n-2) }
assert fib(15) == 610 // 太慢了
我們可以在閉包上呼叫memoize()
方法來生成一個新閉包,該閉包具有快取執行結果的行為。快取使用近期最少使用演算法(LRU)。
fib = { long n -> n<2?n:fib(n-1)+fib(n-2) }.memoize()
assert fib(25) == 75025 //很快
快取會使用閉包的實際引數的值,因此我們在使用非基本型別引數的時候必須格外小心,避免構造大量物件或者進行無謂的裝箱、拆箱操作。
還有幾個方法提供了不同的快取行為。
memoizeAtMost
生成一個最多快取N個物件的新閉包。memoizeAtLeast
生成一個最少快取N個物件的新閉包。memoizeBetween
生成一個新閉包,快取個數在給定的兩者之間。
複合
閉包還可以複合。學過高數的話應該很好理解,這就是多個函式的複合(f(g(x))和g(f(x))的區別)。
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,可惜我沒明白是什麼意思。不過這裡的意思就是尾遞迴,所以我就這麼叫了。遞迴函式在呼叫層數過多的時候,有可能會用盡棧空間,導致丟擲StackOverflowException
。我們可以使用閉包的尾遞迴來避免爆棧。
普通的遞迴函式,需要在自身中呼叫自身,因此必須有多層函式呼叫棧。如果遞迴函式的最後一個語句是遞迴呼叫本身,那麼就有可能執行尾遞迴優化,將多層函式呼叫轉化為連續的函式呼叫。這樣函式呼叫棧只有一層,就不會發生爆棧異常了。
尾遞迴需要呼叫閉包的trampoline()
方法,它會返回一個TrampolineClosure
,具有尾遞迴特性。注意這裡我們需要將外層閉包和遞迴閉包都呼叫trampoline()
方法,才能正確的使用尾遞迴特性。然後我們計算一個很大的數字,就不會出現爆棧錯誤了。
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