Groovy 闭包

简介: 本文介绍了Groovy闭包的有关内容。闭包可以说是Groovy中最重要的功能了。如果没有闭包,那么Groovy除了语法比Java简单点之外,没有任何优势。

本文介绍了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
相关文章
|
3月前
|
缓存 JavaScript 前端开发
|
4月前
|
自然语言处理 JavaScript 前端开发
JavaScript基础知识:什么是闭包(Closure)?
JavaScript基础知识:什么是闭包(Closure)?
26 0
|
4月前
|
JavaScript 前端开发 Java
学习Javascript闭包(Closure)
学习Javascript闭包(Closure)
28 0
Groovy 之基础语法,闭包(下)
Groovy 之基础语法,闭包(下)
|
Java 大数据 索引
Groovy 之基础语法,闭包(上)
Groovy 之基础语法,闭包(上)
【Groovy】闭包 Closure ( 闭包作为函数参数 | 代码示例 )
【Groovy】闭包 Closure ( 闭包作为函数参数 | 代码示例 )
144 0
【Groovy】闭包 Closure ( 闭包作为函数参数 | 代码示例 )
【Groovy】闭包 Closure ( 闭包定义 | 闭包类型 | 查看编译后的字节码文件中的闭包类型变量 )
【Groovy】闭包 Closure ( 闭包定义 | 闭包类型 | 查看编译后的字节码文件中的闭包类型变量 )
130 0
【Groovy】闭包 Closure ( 闭包定义 | 闭包类型 | 查看编译后的字节码文件中的闭包类型变量 )
【Groovy】闭包 Closure ( 闭包调用 | 闭包默认参数 it | 代码示例 )
【Groovy】闭包 Closure ( 闭包调用 | 闭包默认参数 it | 代码示例 )
128 0
【Groovy】闭包 Closure ( 闭包调用 | 闭包默认参数 it | 代码示例 )
【Groovy】Groovy 脚本调用 ( Groovy 脚本中的作用域 | 本地作用域 | 绑定作用域 )
【Groovy】Groovy 脚本调用 ( Groovy 脚本中的作用域 | 本地作用域 | 绑定作用域 )
337 0
【Groovy】Groovy 脚本调用 ( Groovy 脚本中的作用域 | 本地作用域 | 绑定作用域 )
|
机器学习/深度学习
【Groovy】闭包 Closure ( 闭包参数绑定 | curry 函数 | rcurry 函数 | ncurry 函数 | 代码示例 )(一)
【Groovy】闭包 Closure ( 闭包参数绑定 | curry 函数 | rcurry 函数 | ncurry 函数 | 代码示例 )(一)
141 0