闭包官方文档:Closures
这一章节介绍Groovy闭包。Groovy中的闭包是开放,匿名,且可以带参数的代码块,返回一个值并可被赋值给变量。一个闭包可以是被大括号包围的几个变量的声明。不太正式的闭包,在Groovy语言中可以包含无限多个定义在大括号外的变量。标准的闭包定义,提供了一系列的优点,将会在下面介绍。(这一段翻译的太差,请略过)
一个闭包的定义遵从一下语法规则:
{ [closureParameters -> ] statements }
[closureParameters->]是一个可选择的逗号间隔的一系列参数,statements是一个或者多个Groovy声明。闭包的参数和方法的参数列表相似,参数可以是明确类型的也可以是不明确的。
当参数列表是指定的,->是必须的,其被当做是参数和闭包体的分隔。声明由0,或多个Groovy声明组成。
下面是一些有效的闭包定义类型:
{ item++ }//一个指向名为item的变量的闭包
{ -> item++ }//显示的间隔code通过使用箭头(->)
{ println it } //一个使用隐式参数(it)的闭包
{ it -> println it }//it是一个显式变量,这种选法是可选的
{ name -> println name }//这种写法较好
{ String x, int y -> //闭包接受两个明确类型的参数
println "hey ${x} the value is ${y}"
}
{ reader -> // 一个闭包可以接受多行声明
def line = reader.readLine()
line.trim()
}
一个闭包是groovy.lang.Closure的实例,可被赋值给一个变量或字段,尽管看起来像一个代码块:
def listener = { e -> println "Clicked on $e.source" } //你可以将闭包赋值给一个变量
assert listener instanceof Closure//闭包是groovy.lang.Closure的实例
Closure callback = { println 'Done!' }
Closure isTextFile = {//闭包的返回值类型是可选的
File it -> it.name.endsWith('.txt')
}
闭包,是匿名的代码块,可以像方法一样被调用。如果你定义一个闭包像这样没有参数:
def code = { 123 }
然后闭包内的代码将会在调用这个闭包时执行,可以当做一个普通的方法:
assert code() == 123
可选的你也可以显示的使用call方法调用:
assert code.call() == 123
以上这些规则,对于有参数的闭包也是适用的:
def isOdd = { int i-> i%2 == 1 } //定义一个接受int类型作为参数的闭包
assert isOdd(3) == true //可以被直接调用
assert isOdd.call(2) == false //或者使用call方法调用
def isEven = { it%2 == 0 } //使用隐式参数定义一个闭包
assert isEven(3) == false //直接调用
assert isEven.call(2) == true//使用call方法调用
与方法不同的是,闭包在调用时总会返回一个值。下一部分将会讨论怎样声明一个带参数的闭包,以及使用和隐式参数”it”
闭包的参数与普通方法一样遵从同样的规则:
参数之艰难使用逗号隔开:
def closureWithOneArg = { str -> str.toUpperCase() }
assert closureWithOneArg('groovy') == 'GROOVY'
def closureWithOneArgAndExplicitType = { String str -> str.toUpperCase() }
assert closureWithOneArgAndExplicitType('groovy') == 'GROOVY'
def closureWithTwoArgs = { a,b -> a+b }
assert closureWithTwoArgs(1,2) == 3
def closureWithTwoArgsAndExplicitTypes = { int a, int b -> a+b }
assert closureWithTwoArgsAndExplicitTypes(1,2) == 3
def closureWithTwoArgsAndOptionalTypes = { a, int b -> a+b }
assert closureWithTwoArgsAndOptionalTypes(1,2) == 3
def closureWithTwoArgAndDefaultValue = { int a, int b=2 -> a+b }
assert closureWithTwoArgAndDefaultValue(1) == 3
当一个闭包没有显示的定义一个参数列表(使用->),闭包通常使用隐式的参数,名为”it”。代码风格如下:
def greeting = { "Hello, $it!" }
assert greeting('Patrick') == 'Hello, Patrick!'
与下列代码,严格等价:
def greeting = { it -> "Hello, $it!" }
assert greeting('Patrick') == 'Hello, Patrick!'
如果你想声明一个闭包不需要参数,你也应该在调用时不能使用参数,这时需使用无参调用。
def magicNumber = { -> 42 }
// this call will fail because the closure doesn't accept any argument调用失败
magicNumber(11)
闭包也可以像方法一样可以声明可变参数。如果闭包的最后一个参数是可变的,像下面这个类型:
def concat1 = { String... args -> args.join('') }//一个接受可变数量字符串作为第一参数的闭包
assert concat1('abc','def') == 'abcdef'
def concat2 = { String[] args -> args.join('') }//该闭包可以被调用传入任意数量参数,而无需显式的包裹这个数组
assert concat2('abc', 'def') == 'abcdef'
def multiConcat = { int n, String... args ->//效果同上
args.join('')*n
}
assert multiConcat(2, 'abc','def') == 'abcdefabcdef'
Groovy定义闭包作为groovy.lang.Clousure的实例对象出现。与在Java8中出现的lambda表达式不同。关于lambda请参考:Java 8 Lambda表达式探险。Delegation是Groovy闭包的关键概念,这在lambdas中是没有的。闭包改变delegate或者改变delegation strategy的能力使得把Groovy设计成特定领域语言(DSL)成为了可能。
理解delegate的概念,我们首先应该解释一下this在闭包内部的含义。闭包内部通常会定义一下3种类型:
this 对应于闭包定义处的封闭类
owner corresponds to the enclosing object where the closure is defined, which may be either a class or a closure
owner 对应于闭包定义处的封闭对象(可能是一个类或者闭包)
delegate corresponds to a third party object where methods calls or properties are resolved whenever the receiver of the message is not defined
网上关于这方面的只是介绍:
Groovy闭包中的this,owner和delegate
Groovy闭包深入浅出
在闭包中,调用getThisObject将会返回闭包定义处所处的类。等价于使用显示的this:
class Enclosing {
void run() {
def whatIsThisObject = { getThisObject() } //①
assert whatIsThisObject() == this //②
def whatIsThis = { this } //③
assert whatIsThis() == this //④
}
}
class EnclosedInInnerClass {
class Inner {
Closure cl = { this } //⑤
}
void run() {
def inner = new Inner()
assert inner.cl() == inner //⑥
}
}
class NestedClosures {
void run() {
def nestedClosures = {
def cl = { this } //⑦
cl()
}
assert nestedClosures() == this //⑧
}
}
注:
闭包可能的调用封闭类中的方法方式:
class Person {
String name
int age
String toString() { "$name is $age years old" }
String dump() {
def cl = {
String msg = this.toString()//在闭包中使用this调用toString方法,将会调用闭包所在封闭类对象的toString方法,也就是Person的实例
println msg
msg
}
cl()
}
}
def p = new Person(name:'Janice', age:74)
assert p.dump() == 'Janice is 74 years old'
闭包中的owner和闭包中的this的定义非常的像,只不过有一点微妙的不同:它将返回它最直接的封闭的对象,可以是一个闭包也可以是一个类的:
class Enclosing {
void run() {
def whatIsOwnerMethod = { getOwner() }//①
assert whatIsOwnerMethod() == this //②
def whatIsOwner = { owner } //③
assert whatIsOwner() == this //④
}
}
class EnclosedInInnerClass {
class Inner {
Closure cl = { owner } //⑤
}
void run() {
def inner = new Inner()
assert inner.cl() == inner //⑥
}
}
class NestedClosures {
void run() {
def nestedClosures = {
def cl = { owner }//⑦
cl()
}
assert nestedClosures() == nestedClosures //⑧
}
}
闭包的delegate可以通过使用闭包的delegate属性或者调用getDelegate方法。这是Groovy构建为领域特定语言的一个有力的概念。closure-this和closure-owner指向的是语义上的闭包范围,而delegate是用户自定义的供闭包使用的对象。默认的,delegate被设置为owner:
class Enclosing {
void run() {
def cl = { getDelegate() } //①
def cl2 = { delegate } //②
assert cl() == cl2() //③
assert cl() == this //④
def enclosed = {
{ -> delegate }.call()//⑤
}
assert enclosed() == enclosed //⑥
}
}
闭包的delegate可以被更改为任意的对象。先定义两个相互之间没有继承关系的类,二者都定义了一个名为name的属性:
class Person {
String name
}
class Thing {
String name
}
def p = new Person(name: 'Norman')
def t = new Thing(name: 'Teapot')
然后,定义一个闭包通过delegate获取一下name属性:
def upperCasedName = { delegate.name.toUpperCase() }
然后,通过改变闭包的delegate,你可以看到目标对象发生了改变:
upperCasedName.delegate = p
assert upperCasedName() == 'NORMAN'
upperCasedName.delegate = t
assert upperCasedName() == 'TEAPOT'
At this point, the behavior is not different from having a `variable defined in the lexical scope of the closure:
在这一点上,表现不同于定义在闭包括号内的变量:(这句话不好翻译)
def target = p
def upperCasedNameUsingVar = { target.name.toUpperCase() }
assert upperCasedNameUsingVar() == 'NORMAN'
主要的不同如此:
无论何时,在闭包中,访问一个属性,不需要指定接收对象,这时使用的是delegation strategy:
class Person {
String name
}
def p = new Person(name:'Igor')
def cl = { name.toUpperCase() } //name不是闭包括号内的一个变量的索引
cl.delegate = p //改变闭包的delegate为Person的实例
assert cl() == 'IGOR'//调用成功
之所以可以这样调用的原因是name属性将会自然而然的被delegate的对象征用。这样很好的解决了闭包内部属性或者方法的调用。不需要显示的设置(delegate.)作为接收者:调用成功是因为默认的闭包的delegation strategy使然。闭包提供了多种策略方案你可以选择:
(属性没有前缀时调用的策略机制—-我的理解)
- Closure.OWNER_FIRST 是默认的策略。如果一个属性/方法存在于owner,然后他将会被owner调用。如果不是,然后delegate将会被使用
- Closure.Delegate_FIRST 使用这样的逻辑:delegate首先使用,其次是owner
- Closure.OWNER_ONLY 只会使用owner:delegate会被忽略
- Closure.DELEGATE_ONLY 只用delegate:忽略owner
- Closure.TO_SELF can be used by developers who need advanced meta-programming techniques and wish to implement a custom resolution strategy: the resolution will not be made on the owner or the delegate but only on the closure class itself. It makes only sense to use this if you implement your own subclass of Closure.
使用下面的代码来描绘一下”owner first”:
class Person { String name def pretty = { "My name is $name" } //定义一个执行name的闭包成员 String toString() { pretty() } } class Thing { String name //类和Person和Thing都定义了一个name属性 } def p = new Person(name: 'Sarah') def t = new Thing(name: 'Teapot') assert p.toString() == 'My name is Sarah'//使用默认的机制,name属性首先被owner调用 p.pretty.delegate = t //设置delegate为Thing的实例对象t assert p.toString() == 'My name is Sarah'//结果没有改变:name被闭包的owner调用
然而,改变closure的解决方案的策略改变结果是可以的:
p.pretty.resolveStrategy = Closure.DELEGATE_FIRST assert p.toString() == 'My name is Teapot'
通过改变resolveStrategy,我们可以改变Groovy”显式this”的指向:在这种情况下,name将会首先在delegate中找到,如果没有发现则是在owner中寻找。name被定义在delegate中,Thing的实例将会被使用。
“delegate first”和”delegate only”或者”owner first”和”owner only”之间的区别可以被下面的这个其中一个delegate没有某个属性/方法的例子来描述:
class Person { String name int age def fetchAge = { age } } class Thing { String name } def p = new Person(name:'Jessica', age:42) def t = new Thing(name:'Printer') def cl = p.fetchAge cl.delegate = p assert cl() == 42 cl.delegate = t assert cl() == 42 cl.resolveStrategy = Closure.DELEGATE_ONLY cl.delegate = p assert cl() == 42 cl.delegate = t try { cl() assert false } catch (MissingPropertyException ex) { // "age" is not defined on the delegate }
在这个例子中,我们定义了两个都有name属性的类,但只有Person具有age属性。Person类同时声明了一个指向age的闭包。我们改变默认的方案策略,从”owner first”到”delegate only”。由于闭包的owner是Person类,如果delegate是Person的实例,将会成功调用这个闭包,但是如果我们调用它,且它的delegate是Thing的实例,将会调用失败,并抛出groovy.lang.MissingPropertyException。尽管这个闭包定义在Person类中,但owner没有被使用。
关于怎样使用以上这些特点开发DSLs的完整说明可以在a dedicated section of the manual中找到。
先看一下如下代码:
def x = 1
def gs = "x = ${x}"
assert gs == 'x = 1'
结果正如你所想象的那样,但是如果哦我们添加如下代码将会发生什么:
x = 2
assert gs == 'x = 2'
你将会看到assert失败了!原因有两点:
a GString only evaluates lazily the toString representation of values
the syntax xinaGStringdoesnotrepresentaclosurebutanexpressionto x, evaluated when the GString is created.
意思是GString比较懒只会在创建时计算。 x不是闭包,只是一个表达式 x
在我们的例子中,GString带有一个x的索引。当GString被创建完毕,x的值是1,所以GString被创建且值为1。我们们assert该GString则使用toString转化成String。当我们将x的值更改为2是,我们确实改变了x的值,但是不同于对象,GString仍然指向旧的那个。
如果索引的值改变,GString只会改变他的toString方法所代表值。如果索引发生改变,什么都不会发生。
如果你需要一个真正的闭包在GString中,下面的例子强制使用变量的延迟计算,你需要使用语法${->x}:
def x = 1
def gs = "x = ${-> x}"
assert gs == 'x = 1'
x = 2
assert gs == 'x = 2'
描述一下下面这段代码的变化:
class Person {
String name
String toString() { name }//Person类的toString方法返回name属性
}
def sam = new Person(name:'Sam') //创建第一个Person对象名为Sam
def lucy = new Person(name:'Lucy') //另一个名为Lucy的对象
def p = sam //变量赋值为sam
def gs = "Name: ${p}"//(官方文档在这个地方犯错误了)
assert gs == 'Name: Sam'
p = lucy
assert gs == 'Name: Sam'
sam.name = 'Lucy'
assert gs == 'Name: Lucy'
所以,如果你希望依赖变化的对象或者封装的对象,你应该使用显式声明无参闭包在GString中。
class Person {
String name
String toString() { name }
}
def sam = new Person(name:'Sam')
def lucy = new Person(name:'Lucy')
def p = sam
// Create a GString with lazy evaluation of "p"
def gs = "Name: ${-> p}"
assert gs == 'Name: Sam'
p = lucy
assert gs == 'Name: Lucy'
闭包可以被转成接口或者单抽象方法的类型。详情请访问用户手册的这部分。
像Java8中的lambda表达式–闭包,是Groovy函数式编程范式的核心。一些函数式编程关于函数的操作直接适用于Closure类,就像本部分描述的。
对于科里化不了解的同学,请参考Lambda演算与科里化(Currying)百度百科:科里化在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel 和 Gottlob Frege 发明的。
进入正题
在Groovy中,科里化指的是部分程序的概念。科里化在函数式编程中没有对应的具体的概念,因为在Groovy中应用的闭包的不同的范围规则。(这句话好难理解啊)。科里化允许你一个一个的设置参数的值,并且返回一个新的闭包且接收的参数少了一个。
Left currying是先设置闭包最左边的参数,比如下面的例子:
def nCopies = { int n, String str -> str*n }//nCopies闭包定义了两个参数
def twice = nCopies.curry(2)//curry将会设置第一个参数为2,并创建了一个新的闭包,且接收单个参数String
assert twice('bla') == 'blabla' //所以新的闭包可以接收一个String参数
assert twice('bla') == nCopies(2, 'bla')
与左科里化很相似,例子如下:
def nCopies = { int n, String str -> str*n }
def blah = nCopies.rcurry('bla')
assert blah(2) == 'blabla'
assert blah(2) == nCopies(2, 'bla')
为防止一个闭包超过2个参数,使用任意参数的科里化ncurry成为了可能:
def volume = { double l, double w, double h -> l*w*h }
def fixedWidthVolume = volume.ncurry(1, 2d)
assert volume(3d, 2d, 4d) == fixedWidthVolume(3d, 4d)
def fixedWidthAndHeight = volume.ncurry(1, 2d, 4d)
assert volume(3d, 2d, 4d) == fixedWidthAndHeight(3d)
Memoization 允许将调用闭包获得的结果缓存起来。如果调用方法(闭包)进行计算很慢,但是又需要经常调用且使用同样的参数。一个典型的例子就是裴波那契数的计算。一个粗糙(幼稚)的实现,看起来像这样:
def fib
fib = { long n -> n
闭包的组成对应于函数的组成的概念,也就是说创建一个新的函数通过叠加两个或多个函数(链式调用),例如:
def plus2 = { it + 2 }
def times3 = { it * 3 }
def times3plus2 = plus2
递归算法通常收到物理环境的限制:堆栈的最大值。例如,你调用一个递归方法太深,最终会抛出StackOverflowException。
通过使用闭包和它的trampoline能力将是一个不错的方法。
闭包被封装在TrampolineClosure。在调用时,一个trampolined的闭包将会调用原始的闭包并等待结果。如果调用的输出是另一个TrampolineClosure的实例,这个新生成的闭包将作为结果调用trampoline()方法,这个Closure将会被调用。重复的调用并返回trampolined 闭包实例将会持续直到返回一个值而不是一个trampolined Closure。这个值将成为trampoline的最终结果。这种方式的调用连续且不会内存溢出。
下面是一个使用trampoline()来实现阶乘的例子:
def factorial
factorial = { int n, def accu = 1G ->
if (n
通常实际应用中会用到规则的方法作为闭包。例如,你可能想要使用闭包的科里化能力,但是科里化对于普通方法是不合适的。在Groovy中,你可以通过方法指针操作符生成一个闭包。