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