1. 程式人生 > >Groovy 閉包

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方法來實現,它會固定指定索引的引數。另外還有curryrcurry方法,用於固定最左邊和最右邊的引數。

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