闭包是Groovy的精髓,核心概念之一。它是构建gradle DSL的必备特性,我们将通过一整个小结来学习它。
目录
1. 闭包的基础语法
Groovy中的闭包是一个匿名代码块,由一对大括号{}包围。它可以接收参数,返回值(看起来非常像函数),甚至可以赋值给一个变量。
1.1 定义闭包
定义闭包的语法如下:
{ [closureParameters -> ] statements }
方括号[closureParameters -> ]表示可选参数列表,类似方法的参数列表,参数可以有类型,也可以无类型。一旦指定了参数,1个或多个,->箭头符号就是必须的。下面给出了一些正确的闭包定义:
{ item++ } //不指定参数列表且省略->箭头符号
{ -> item++ } //参数列表为空,但写出->符号,表示不使用任何参数,包括默认参数it。(隐式参数下面会讲)
{ println it } //打印默认参数it
{ it -> println it } //显式指定it参数
{ name -> println name } //显式指定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" } //将闭包赋值给变量listener
assert listener instanceof Closure
Closure callback = { println 'Done!' } //不通过def或var,我们可以直接使用Closure类型
Closure<Boolean> isTextFile = {
File it -> it.name.endsWith('.txt')
} //Closure有返回值,你可以明确指定Closure的返回值
1.2 调用闭包
闭包作为代码块自然可以像其他函数一样被调用。例如你定义了一个无参闭包:
def code = { 123 }
你有两种方法来调用它:
assert code() == 123 //直接通过闭包名加一对圆括号
assert code.call() == 123 //或者你可以显式的调用Closure的call方法
对于有参的闭包,调用原则是一样的,例如:
// 显式指定参数列表
def isOdd = { int i -> i%2 != 0 }
assert isOdd(3) == true
assert isOdd.call(2) == false
// 使用隐式参数 it
def isEven = { it%2 == 0 }
assert isEven(3) == false
assert isEven.call(2) == true
与方法不同的是,闭包被调用时始终会返回值。
1.3 闭包参数
1.3.1 普通参数
闭包参数遵循普通方法的参数规则:参数名称,可选的参数类型,可选的参数默认值,多个参数之间用逗号分隔:
def closureWithOneArg = { str -> str.toUpperCase() } //无类型,无默认值
assert closureWithOneArg('groovy') == 'GROOVY'
def closureWithOneArgAndExplicitType = { String str -> str.toUpperCase() } //指定类型String,无默认值
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
从示例来看,groovy闭包的参数定义非常自由。
1.3.2 隐式参数
当没有显式指定任何参数列表时,闭包会定义一个隐式参数 it。我们可以在闭包中引用它:
def greeting = { "Hello, $it!" }
assert greeting('Patrick') == 'Hello, Patrick!'
等价于 ===>
def greeting = { it -> "Hello, $it!" }
assert greeting('Patrick') == 'Hello, Patrick!'
如果你一定要定义一个无参的闭包,你可以明确指定参数列表为空:
def magicNumber = { -> 42 }
// 这里的调用会失败,因为magicNumber已经明确声明不接受任何参数
magicNumber(11)
1.3.3 可变参数
def concat1 = { String... args -> args.join('') } //第一个参数为可变参数
assert concat1('abc','def') == 'abcdef'
def concat2 = { String[] args -> args.join('') } //可变参数与args显式指定为数组相同
assert concat2('abc', 'def') == 'abcdef'
def multiConcat = { int n, String... args -> //最后一个参数为可变参数
args.join('')*n
}
assert multiConcat(2, 'abc','def') == 'abcdefabcdef'
可不可以第一个为可变参数,第二个为不可变参数?不可以,可变参数必须是最后一个参数。
1.4 委托
委托是groovy闭包的一个非常重要的特性。通过改变闭包的委托以及委托策略,我们可以设计出优雅的DSL。为了理解委托,我们首先来认识下闭包中的三个关键字:
- this
- owner
- delegate
1.4.1 this的含义
this对应定义当前闭包的外部类。调用getThisObject等价于this。我们看下面这个例子:
class Enclosing {
void run() {
def whatIsThisObject = { getThisObject() } //我们在Enclosing类中定义了一个闭包,它调用了getThisObject方法。
assert whatIsThisObject() == this //调用闭包,闭包的返回值即闭包中最后一行代码的执行结果,这里返回定义闭包的类Enclosing的实例。
def whatIsThis = { this } //这种方式更简洁,this == getThisObject
assert whatIsThis() == this
}
}
class EnclosedInInnerClass {
class Inner {
Closure cl = { this } //在内部类中定义了一个闭包
}
void run() {
def inner = new Inner()
assert inner.cl() == inner //闭包中的this返回inner类的实例,而不是顶层类EnclosedInInnerClass
}
}
class NestedClosures {
void run() {
def nestedClosures = {
def cl = { this } //对于嵌套的闭包,比如这里在nestedClosures闭包中定义的闭包,this返回的仍然是最近的外部类NestedClosures,而不是nestedClosures闭包。
cl()
}
assert nestedClosures() == this
}
}
既然this返回的是外部类的实例,我们可以通过this来访问外部类的属性和方法:
class Person {
String name
int age
String toString() { "$name is $age years old" }
String dump() {
def cl = {
String msg = this.toString() //闭包中调用了this的toString方法,实际上是调用的外部类Person的方法
println msg
msg
}
cl()
}
}
def p = new Person(name:'Janice', age:74)
assert p.dump() == 'Janice is 74 years old'
1.4.2 Owner的含义
Owner的定义与this非常相似,仅有一点细微的差别。它返回的是直接外部对象,可能是一个类,也可能是一个闭包:
class Enclosing {
void run() {
def whatIsOwnerMethod = { getOwner() } //getOwner等价于owner
assert whatIsOwnerMethod() == this //这里返回的是Enclosing类
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 } //对于嵌套闭包,这里的owner返回的是nestedClosures,注意这里与this的区别!!!
cl()
}
assert nestedClosures() == nestedClosures
}
}
1.4.3 Delegate的含义
Delegate对应一个外部对象,当闭包中的属性和方法在执行中找不到所属形象时,就会尝试调用第三方对象同名的属性和方法。简单说,就是将方法和属性的调用,委托给了第三方对象的同名属性和方法。我们可以通过delegate属性或getDelegate方法来获取闭包的委托。它是一个非常强大的特性,通过它来构建DSL。this和owner属于词法作用域,也就是说可以在编译期可以推断指向。而delegate指向的是供闭包使用的用户自定义对象(第二节会详细说明)。默认情况下delegate等价于owner。
class Enclosing {
void run() {
def cl = { getDelegate() } //getDelegate方法等价于delegate属性
def cl2 = { delegate }
assert cl() == cl2()
assert cl() == this //delegate与owner指向相同,因为直接外层对象是Enclosing,所以这里也等同于this
def enclosed = {
{ -> delegate }.call() //delegate与owner指向相同,直接外层对象为enclosed闭包
}
assert enclosed() == enclosed
}
}
闭包的委托可以改变为任意对象。看如下的例子:
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() }
//然后我们改变闭包的委托
upperCasedName.delegate = p
assert upperCasedName() == 'NORMAN'
upperCasedName.delegate = t
assert upperCasedName() == 'TEAPOT'
//我们可以通过如下的代码实现类似上面的效果,那为什么还需要委托?
def target = p
def upperCasedNameUsingVar = { target.name.toUpperCase() }
assert upperCasedNameUsingVar() == 'NORMAN'
//在这个例子中,target是一个本地变量,需要我们在闭包中引用它。而委托可以动态传递,我们甚至可以省略delegate.,直接写name.toUpperCase。
1.4.4 委托策略
无论何时,我们在闭包中使用一个属性时,可以不明确指定它的所属对象,那这时,该如何确定该属性的所属对象呢?这就要取决于委托策略。
class Person {
String name
}
def p = new Person(name:'Igor')
def cl = { name.toUpperCase() } //这里我们在闭包中直接使用name属性,词法作用域无法引用name,因为并没有定义。
cl.delegate = p //不过我们可以通过改变闭包的委托,指向Person的实例
assert cl() == 'IGOR' //这时闭包方法就可以成功被调用了
这里很神奇,之所以能运行是因为name被解析为delegate对象(Person实例)的属性(属性方法同样有效)。我们不再需要显式的指定delegate.;属性仍然可以正确被访问,这得益于闭包的委托策略。闭包提供了多种策略可以选择:
- OWNER_FIRST 这是默认策略。如果属性和方法存在于owner中,则调用owner的属性和方法。如果没找到,会调用delegate对应的属性和方法。
- DELEGATE_FIRST 与OWNER_FIRST逻辑刚好相反,会优先使用delegate,然后再考虑owner。
- OWNER_ONLY 只会去解析owner中的属性和方法。delegate会被忽略。
- DELEGATE_ONLY 只会解析delegate中的属性和方法。owner会被忽略。
- TO_SELF 常用语元编程,实现自定义策略。(暂不涉及,有兴趣的同学可以自行检索)
我们看下面这个例子:
class Person {
String name
def pretty = { "My name is $name" } //这里定义了一个类型为闭包的类属性,Person和Thing都定义了name属性。
String toString() {
pretty()
}
}
class Thing {
String name
}
def p = new Person(name: 'Sarah')
def t = new Thing(name: 'Teapot')
assert p.toString() == 'My name is Sarah' //使用默认策略,name属性属于owner对象p
p.pretty.delegate = t
assert p.toString() == 'My name is Sarah' //虽然我们这边改变了委托对象,但仍然优先匹配owner对象的name属性
我们修改一下pretty闭包的委托策略,结果就会发生变化:
p.pretty.resolveStrategy = Closure.DELEGATE_FIRST
assert p.toString() == 'My name is Teapot' //这里我们会优先查找委托中的属性,而委托指向了t。
下面的例子演示了,DELEGATE_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 = p.fetchAge
cl.delegate = p //将cl的委托指向p,p拥有name和age两个属性
assert cl() == 42 //默认策略是owner first,所以这里打印Person的age属性
cl.delegate = t //将cl的委托指向t,t并不拥有name属性
assert cl() == 42 //默认策略是owner first,所以这里仍然打印Person的age属性
cl.resolveStrategy = Closure.DELEGATE_ONLY
cl.delegate = p
assert cl() == 42 //改变委托策略后,这里打印p的属性
cl.delegate = t
try {
cl() //这里只会调用委托对象t的age属性,但age在t中并没有定义,所以抛出异常
assert false
} catch (MissingPropertyException ex) {
// "age" 未定义
}
1.5 柯里化
在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数的函数,并且返回接受余下的参数且返回结果的新函数的技术。简单说就是,可预先配置部分参数,在使用时接受剩余函数。groovy闭包也提供了柯里化的方法,根据预先配置最左边参数还是最右边参数,又可分为左柯里化curry和右柯里化rcurry:
def nCopies = { int n, String str -> str*n } //定义一个闭包,接受两个参数
def twice = nCopies.curry(2) //柯里化方法会设置第一个参数的值为2,并返回一个新的闭包,这个闭包接受一个参数str
assert twice('bla') == 'blabla'
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,我们是否可以指定具体某个参数的值,执行柯里化操作呢?groovy闭包提供了ncurry方法:
def volume = { double l, double w, double h -> l*w*h }
def fixedWidthVolume = volume.ncurry(1, 2d) //指定第二个参数的值2,并返回新闭包
assert volume(3d, 2d, 4d) == fixedWidthVolume(3d, 4d)
def fixedWidthAndHeight = volume.ncurry(1, 2d, 4d) //指定从第二个参数开始,后续参数的值,并返回新的闭包
assert volume(3d, 2d, 4d) == fixedWidthAndHeight(3d)
2. 闭包的本质
通过上面的学习,你知道了如何定义闭包,如何调用它,以及如何使用委托,那到底什么是闭包?闭包形式上与函数非常相似,他们之间有区别吗?
闭包本质上是一个函数以及该函数关联的词法作用域的组合,闭包使得你能够在一个内部函数中访问外部函数的作用域。函数大家都懂,无非是一系列操作的一个集合。那么什么是词法作用域?举个例子:
void test() {
String testName = 'Groovy' // testName是一个由test函数创建的局部变量
Closure displayName = { // 我们在test函数内部定义了一个闭包displayName
println "$testName" // 这里打印了外部函数test定义的testName变量
}
displayName()
}
test()
这是词法作用域的一个典型例子,即由变量在源代码中声明的位置来决定变量是否可见。内部函数可以访问外层作用域中声明的变量。这里displayName是内部函数,它访问了外部函数test中定义的testName变量。词法作用域也称为静态作用域,其实很好理解,在C,Java等语言中,使用{}大括号来形成新的代码块,内层的代码块可以使用外层代码块中的变量,道理是类似的。我们对上面的代码稍做修改:
Closure test() {
String testName = 'Groovy' // name是一个由test函数创建的局部变量
int totalCount = 0
Closure displayName = { // 我们在test函数内部定义了一个闭包displayName
count ->
println "$testName" // 这里打印了外部函数test定义的name变量
totalCount += count
println "totalCount = $totalCount"
}
return displayName
}
Closure cl = test()
cl(5)
这里,我们修改了test方法,让它返回闭包,并且增加了一个属性totalCount。闭包中我们累加参数count到totalCount,然后将其打印。最后我们调用了cl,结果打印如下:
Groovy
totalCount = 5
乍一看,好像没啥问题啊,totalCount初值为0,加上参数传入的5,结果为5。打印的结果也如预期。如果你了解C或者Java,你会发现一个函数内定义的局部变量只存在于函数执行过程中(典型的实现是将函数的返回地址压入调用栈,然后函数内定义的局部变量依此入栈),当函数执行完成后局部变量就不再可见(函数调用栈局部变量全部出栈)。这里显然不是这个逻辑,test执行完成后返回了闭包cl,然后才调用的闭包cl,test退出totalCount已经不可见了,执行会出错。
闭包是如何实现的?一个闭包定义了一个函数,以及该函数定义时的词法作用域,在该作用域中的所有变量的状态都会被保存下来。例如这里,闭包定义时的词法作用域中引用的变量testName,totalName。在我们再次调用闭包时,这些变量的状态会被恢复。如果我们再次调用cl(5)会发生什么?
Closure cl = test()
cl(5)
cl(5)
//输出结果
Groovy
totalCount = 5
Groovy
totalCount = 10
test closure...
这里就不难理解了,我们在第一次执行完成后,totalCount的值被保存下来。当再次调用cl(5)时,totalCount的值又被恢复为5,所以这里输出了结果10。
3. 闭包的优点及使用场景
1. 使用闭包及其委托特性可是实现优雅的DSL,例如我们正在学习的Gradle
2. 简化容器遍历处理,代码更简洁易读
class Fruit {
String name
String color
}
class GroovyDemo {
static void main(args) {
def map = ["china":["name": 'shanghai', "rank": 1], "lover":["usa": 'newyork', "rank": 2]]
map.each { key, value -> println(key+"="+value) }
def alist = ["apple","banana","orange","grape","lemon"]
alist.each { println(it) }
(1..10).each { println(it) }
def persons = [new Fruit("name": 'apple', "color": "red"), new Fruit("name": 'banana', "color": "yellow")]
println persons.collect { it.name }
println persons.find { it.color =="yellow" }.name
(1..10).groupBy { it % 2 == 0 } .each {
key, value -> println(key.toString() + "=" + value)
} //链式调用,简洁易懂
}
}
3. 易于实现可扩展可复用的代码
模板方法模式将算法的可变与不可变部分分离出来。 通常遵循如下模式: doCommon1 -> doDiff1 -> ... DoDiff2 -> ... -> DoCommon2 。 Java 实现模板方法模式,通常需要先定义一个抽象类,在抽象类中定义好算法的基本流程,然后定义算法里那些可变的部分,由子类去实现。使用闭包可以非常轻松地实现模板方法模式,只要将可变部分定义成 闭包即可
def static templateMethod(list, common1, diff1, diff2, common2) {
common1 list
diff1 list
diff2 list
common2 list
}
def common1 = { list -> list.sort() }
def common2 = { println it }
def diff1 = { list -> list.unique() }
def diff2 = { list -> list }
templateMethod([2,6,1,9,8,2,4,5], common1, diff1, diff2, common2)
以此类推,我们可以将可变的操作作为闭包传递给函数,而将不可变的内容固化在函数中,极大的增加了代码的灵活性和复用性。
4. 结语
本节学习了闭包的基本使用及相关的重要特性,有些知识点需要反复实验推敲;gradle中大量使用了闭包特性,希望小伙伴们好好掌握。
5. 参考
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures
- https://stackoverflow.com/questions/1047454/what-is-lexical-scope
- https://docs.groovy-lang.org/latest/html/documentation/core-domain-specific-languages.html
- https://stackoverflow.com/questions/36636/what-is-a-closure/36639#36639
- http://www.groovy-lang.org/closures.html
- https://www.cnblogs.com/lovesqcc/p/9721739.html