本文翻译自Groovy,主要介绍了Groovy语言的闭包。
闭包按照以下语法定义:
{ [closureParameters -> ] statements }
closureParameters
是一个可选,由“,”分割的参数列表,statements包含0或者更多数量不等的Groovy表达式。闭包的参数列表看起来和方法的很像,而且参数可以是无类型的。
当参数列表指定了参数,我们需要->
用于分隔参数列表和表达式。statements部分可以包含0,1或者更多的Groovy的表达式。
下面是一些关于定义闭包的例子:
{ item++ } 1
{ -> item++ } 2
{ println it } 3
{ it -> println it } 4
{ name -> println name } 5
{ String x, int y 6->
println "hey ${x} the value is ${y}"
}
{ reader 7->
def line = reader.readLine()
line.trim()
}
1.闭包引用一个item
变量
2.增加一个箭头(->)可以从代码块有效的分隔参数
3.it是闭包的默认的参数,当没有传入参数时,it为null
4.当只有一个参数时,一般默认使用it表示参数
5.当然也可以显示的指定其他的名字
6.闭包接收两个指定类型的参数
7.闭包可以包含多个表达式
闭包是groovy.lang.Closure类的实例,他可以赋给一个变量或者一个属性作为其他的变量。尽管他是一个代码块:
def listener = { e -> println "Clicked on $e.source" } 1
assert listener instanceof Closure
Closure callback = { println 'Done!' } 2
Closure isTextFile = {File it -> 3
it.name.endsWith('.txt')
}
1.闭包是groovy.lang.Closure类的实例,可以被赋给一个变量
2.如果不使用def,我们可以把闭包赋值给一个groovy.lang.Closure类型的变量
3.可选的,我们可以指定闭包的返回类型(Boolean)。
闭包作为一个匿名的代码块可以像其它方法那样被调用。可以像下面这样定义一个没有输入参数的闭包:
def code = {1,2,3}
闭包内部的代码只有在被调用时才会执行,他可以被当作一个变量:
ssert code() == 123
或者,你也可以显式的使用call
方法:
assert code.call() == 123
如果闭包接收参数,原理也是一样的:
def isOdd = { int i-> i%2 == 1 } 1
assert isOdd(3) == true 2
assert isOdd.call(2) == false 3
def isEven = { it%2 == 0 } 4
assert isEven(3) == false 5
assert isEven.call(2) == true 6
1.定义一个接收int
类型参数的闭包
2.闭包被直接调用
3.或者使用call
方法调用
4.使用默认的it
,同样能够实现相同的功能
5.使用arg
直接调用
6.或者是使用call
方法
与方法不同,闭包在被调用时总是返回一个结果。下一节将介绍如何声明闭包的参数,什么时候使用它们,以及什么是it
默认参数
闭包的参数遵循与普通方法参数一样的规则:
一个 可选的类型
一个name
一个可选的默认值
参数用逗号隔开:
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('') } 1
assert concat1('abc','def') == 'abcdef' 2
def concat2 = { String[] args -> args.join('') } 3
assert concat2('abc', 'def') == 'abcdef'
def multiConcat = { int n, String... args -> 4
args.join('')*n
}
assert multiConcat(2, 'abc','def') == 'abcdefabcdef'
1.闭包可以接收数量可变的String参数
2.可以在调用时使用任意数量的参数,而且不用被包装成一个数组
3.使用数组也能达到同样的效果
4.只要最后一个参数是一个数组或者可变长度的的参数类型
Groovy定义闭包(作为Closure类的实例),与Java8的lambda有很大的不同。
Delegation 在Groovy闭包中是一个关键的概念(lambda中没有与之对应的概念)。闭包中改变委托或者改变委托策略的能力使得我们可以在Groovy中设计美丽的领域特定语言(DSLs)
要理解delegate的概念,我们首先要说明this在闭包内的含义。闭包实际上定义了三个截然不同的变量(当然it
也是):
this
对应定义闭包的类,不论闭包被嵌套了多少层闭包,都指向此类
owner
对应定义闭包的对象,他可能是闭包或者类。当只有一层闭包时,等同于this
delegate
对应于调用方法的第三方对象,默认等同于owner(可变)
在一个闭包内调用getThisObject
方法将返回定义此闭包的类,与使用this的效果一样:
class Enclosing {
void run() {
def whatIsThisObject = { getThisObject() } 1
assert whatIsThisObject() == this 2
def whatIsThis = { this } 3
assert whatIsThis() == this 4
}
}
class EnclosedInInnerClass {
class Inner {
Closure cl = { this } 5
}
void run() {
def inner = new Inner()
assert inner.cl() == inner 6
}
}
class NestedClosures {
void run() {
def nestedClosures = {
def cl = { this } 7
cl()
}
assert nestedClosures() == this 8
}
}
1.一个闭包被定义Enclosing
类,返回getThisObject
2调用闭包将会返回一个定义此闭包的Enclosing
类的实例
3.通常,你只会想使用this
4.他返回完全相同的类
5.如果闭包被定义在一个内部类
6.闭包内的this将返回次内部类,不是顶级的那个
7.嵌套的闭包,就像cl
被定义在nestedClosures
闭包内
8.this
对应于最近的外部类,而不是闭包
当然也可以在闭包内通过this调用类的方法:
class Person {
String name
int age
String toString() { "$name is $age years old" }
String dump() {
def cl = {
String msg = this.toString() 1
println msg
msg
}
cl()
}
}
def p = new Person(name:'Janice', age:74)
assert p.dump() == 'Janice is 74 years old'
1.闭包通过this
调用toString()方法,实际上调用封闭对象的toString()方法,也就是Person实例。
闭包内的owner
和在闭包内定义的this
非常相似,除了一点微妙的差别:
他将返回定义此闭包的对象,不管他是类或者闭包:
class Enclosing {
void run() {
def whatIsOwnerMethod = { getOwner() } 1
assert whatIsOwnerMethod() == this 2
def whatIsOwner = { owner } 3
assert whatIsOwner() == this 4
}
}
class EnclosedInInnerClass {
class Inner {
Closure cl = { owner } 5
}
void run() {
def inner = new Inner()
assert inner.cl() == inner 6
}
}
class NestedClosures {
void run() {
def nestedClosures = {
def cl = { owner } 7
cl()
}
assert nestedClosures() == nestedClosures 8
}
}
1.一个闭包被定义在Enclosing
类,返回getOwner
2.调用闭包将返回定义此闭包的Enclosing
类的实例
3.通常,你只会想使用owner
4.他将返回相同的对象
5.如果闭包被定义在一个内部类
6.闭包内的owner
将返回次内部类,而不是最顶级的类
7.如果是被嵌套在闭包内,就像被嵌套在闭包nestedClosures
内部的cl
8.owner
对应闭包,这就是owner
和this
的不同
闭包的delegate可以通过使用delegate或者调用getDelegate方法访问。
在Groovy语言中这是一个强大的概念。this
和owner
被定义在闭包的语法范围内,而delegate
是被定义在使用闭包的对象上。默认,delegate
被设置为:owner
:
class Enclosing {
void run() {
def cl = { getDelegate() } 1
def cl2 = { delegate } 2
assert cl() == cl2() 3
assert cl() == this 4
def enclosed = {
{ -> delegate }.call() 5
}
assert enclosed() == enclosed 6
}
}
1.通过闭包调用getDelegate
方法,我们可以得到delegate
2.或者使用delegate
3.返回一样的对象
4.返回类
5.在嵌套的闭包内
6.delegate
和owner
作用相同
闭包的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'
一定意义上说,在闭包内部使用一个自定义的变量(target)也能有同样的效果:
def target = p
def upperCasedNameUsingVar = { target.name.toUpperCase() }
assert upperCasedNameUsingVar() == 'NORMAN'
然而,他们有很大的不同点:
在最后的例子,target
是一个闭包内部的局部变量
而delegate
可以被显示的使用,也就是说可以用不加前缀的方式调用delegate
,在下一节将就此做详细的说明
在闭包内,每当我们没有显示指定对象的去访问一个属性,那么delegate 策略是involved:
String name
}
def p = new Person(name:'Igor')
def cl = { name.toUpperCase() } 1
cl.delegate = p 2
assert cl() == 'IGOR' 3
1.在闭包内,属性name
是没有被定义的,闭包被调用时,会报错
2.当我们把闭包的delegate
变成一个Person
类的实例时
3.方法可以被正常的使用
这个代码成功的原因是闭包内的name
属性将会被显示的指定给与delegate对应的对象(The reason this code works is that the name property will be resolved transparently on the delegate object!)!这个特性对于在闭包内部使用属性或者调用方法是很有帮助的。不需要我们显示的指定delegate
,recelver:调用将会成功,因为在闭包内默认的delegate策略就是如此。闭包实际上定义了多种resolution strategies ,我们可以根据实际需要作出选择:
Closure.OWNER_FIRST
是默认策略。如果属性或者方法存在于owner内,那么他可以被owner调用,如果不存在,则会尝试在delegate类中查找
Closure.DELEGATE_FIRST
颠倒了默认逻辑:delegate是第一优先级,其次才是owner
Closure.OWNER_ONLY
将仅仅在owner查找需要的属性或者方法:delegate会被忽略
Closure.DELEGATE_ONLY
将仅仅在delegate查找需要的属性或者方法:owner会被忽略
Closure.TO_SELF
可以被用于当开发人员需要使用先进的元数据编程技术和希望实现一个自定义的选择策略时:这个选择将不是owner或者delegate,而仅仅是closure类自己。当我们实现了自己的Closure
子类时,他才是有意义的。
让我们通过下面的代码了解下默认的owner first
:
class Person {
String name
def pretty = { "My name is $name" } 1
String toString() {
pretty()
}
}
class Thing {
String name 2
}
def p = new Person(name: 'Sarah')
def t = new Thing(name: 'Teapot')
assert p.toString() == 'My name is Sarah' 3
p.pretty.delegate = t 4
assert p.toString() == 'My name is Sarah' 5
1.在这个例子中,我们定义了一个闭包,在闭包内引用了一个变量name
2.在Person
和Thing
类中都定义了name
成员变量
3.使用默认的策略,name
变量首先在owner中被寻找。
4.我们改变delegate为Thing
类的实例t
5.结果没有改变:name
变量首先在闭包的owner
属性所对应的对象内寻找
然而,我们可以更改闭包的选择策略:
p.pretty.resolveStrategy = Closure.DELEGATE_FIRST
assert p.toString() == 'My name is Teapot'
通过改变resolveStrategy
,我们可以修改Groovy选择引用时的方式:在这个例子里,name
将首先在delegate中寻找,如果没有发现,再在owner中寻找。因为name
在delegate中有被定义(一个Thing类的实例
),所以name
变量将会引用delegate内的name
成员变量。
如果delegate(resp,owner)没有这样的一个方法或者属性,那么delegate first
,delegate only
,owner first
和owner only
的不同点将会被证明:
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
}
在这个例子里,我们定义了两个类:Person
类有name
和age
两个成员变量;Thing
类只有name
一个成员变量,在Person
类里还定义了一个引用了age
变量的闭包。我们把默认的选择策略从owner first
改变为delegate only
。因为闭包的owner是Person
类的实例,我们可以测试一下如果delegate是Person
类的实例,调用闭包时会成功。但是如果我们改变delegate为Thing
类的实例时,调用闭包将会失败,且报groovy.lang.MissingPropertyException
异常。尽管闭包是在Person
类里被定义的,owner是没有被使用。
在官网可以找到如何使用这个功能来开发DSLs
的详细介绍.
看下面的代码:
def x = 1
def gs = "x = ${x}"
assert gs == 'x = 1'
代码将会按照我们所预期的那样运行,但是如果增加下面的代码,将会发生什么呢:
x = 2
assert gs == 'x = 2'
你将看到运行失败!有两个原因:
Gstring只懒加载toString
的值
${X}
在GString中不表示闭包而仅仅是一个符号$x
,在GString被创造时就确定。
在我们的例子中,GString
被创造时包含一个表达式(引用x)。当GString
被创造时,x的值为1,所以GString
被创造时包含值1.当assert触发时,GString
被确定为1通过toString
被转变为String
。当我们把x改变为2时。我们确实改变了x的值,但是他们已经是不同的对象,GString
仍然引用老的哪一个x对象。
Gstring仅仅改变他的String
,如果他引用的对象的值是发生改变,nothing will change。
如果你想在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 } 1
}
def sam = new Person(name:'Sam') 2
def lucy = new Person(name:'Lucy') 3
def p = sam 4
def gs = "Name: ${p}" 5
assert gs == 'Name: Sam' 6
p = lucy 7
assert gs == 'Name: Sam' 8
sam.name = 'Lucy' 9
assert gs == 'Name: Lucy' 10
1.Person
类有一个返回name
成员变量的toString
方法
2.我们创造一个name叫Sam的Person类实例
3.我们创造一个name叫Lucy的Person类实例
4.sam变量被赋值给变量p
5.我们创造了一个闭包,在闭包内引用了p
,也就是sam
6.所以当我们运行次闭包时,他返回sam
7.如果我们把Lucy赋值给p
8.gs任然等于Sam,因为p的价值sam在GString被创造时就已经确定
9.所以如果我们改变sam的名字为Lucy
10.这样GString的值被改变了。
(这段话我也不是很明白,按我的理解:GString里的表达式在被创造时就变成了引用的变量所对应的变量,例如上边例子中的1和下边例子中的sam,所以上边例子改变X没用,但是在下边的例子里,改变sam的属性,结果还是会发生变化,也就是因此。如果大家有什么不一样的简介,欢迎留言。)
so,如果你不想依赖改变对象或者包装对象,你就必须在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'
闭包可以被装换为接口或者单例-抽象方法类型。在官网有更详细的说明。
就像lambda表达式在Java8中一样,闭包是Groovy语言函数式编程范例的核心。一些函数式编程操作功能在Closure
类就可以直接使用,就像本节说明的那样。
我们可以用Memoization技术来代替函数中太多的递归调用。Memoization是一种可以缓存之前运算结果的技术,这样我们就不需要从新计算那些已经计算过的结果。
把常规方法当做闭包是一个很常用的操作。例如,你可能想要使用闭包的柯里化能力,但是这在平常的方法中是不被允许的。在Groovy中,你可以通过方法指针操作把任何的方法变成闭包。
错误是难免的,如果读者发现了,还望留言指出!有什么不甚理解的地方也欢迎留言讨论