使用gradle 构建的项目中会使用groovy语言来完成项目构建的配置文件build.gradle
的编写, 为了更好的写这个配置文件, 所以花了点时间深入学习了一下groovy这么语言.groovy可以看做是JVM上的一门脚本语言, 它可以无缝的使用JDK中的jar包,它也是Java语言的扩展.groovy中具有很多脚本语言的共性(比如弱类型, 闭包等等), groovy中存在两个看起来类似的概念,即是方法与闭包.因此本文总结了groovy中方法与闭包的共同点与区别.
首先看如下的情况
def val = "hello"
def test_func() {
println val
}
test_func()
按照以往脚本的编程经验, 上述函数可以正确的输出结果, 然而在groovy中却报错
groovy.lang.MissingPropertyException: No such property: val for class: ConsoleScript42
定义的test方法不可以获取到方法外定义的val
变量.原因是val
的作用域不包含test_func方法. 理解这一点需要了解groovy的一种运行方式. Groovy本身也是运行于JVM中, 它需要被编译成class字节码文件运行, 当我们将上述的groovy脚本编译成class文件, 然后通过反编译工具查看反编译的Java文件大致如下:
public class test{
public static void main(String... args){
// call run
}
public Object run(){
}
public Object test_func(){
}
}
可以看到创建的Java 类中会有一个run方法, 这个run方法的作用域其实就是编写groovy脚本的顶层属性的scope(val 变量所在的scope), 而test_func这个方法只是类中的一个方法而已,它和run方法是并列的两个不同的方法,作用域不同自然不能够相互访问其中的属性.
一个简单的递归例子, 比如使用递归的方法求解一个list中的最大值, 使用函数的代码实现如下:
def max_val(list, val) {
if(list.size() == 1) return list[0]>val?list[0]:val
else return max_val(list[1..val?list[0]:val)
}
与其他脚本语言一样,比较简单, 但是如果使用闭包实现, 代码如下:
def max_val_clo = {list,val->{
if(list.size() == 1) return list[0]>val?list[0]:val
else return call(list[1..val?list[0]:val)
}
}
关键的就是在闭包里面使用call
指代本身,一些教程里面在这里实现递归可能会用闭包的名字(此处是max_val_clo)或者是this.call
, 但是在最新的groovy中, 这些语法都会出错. 在初次接触groovy的闭包的时候就了解到闭包可以使用call方法接受参数运行,如下:
def clo = {val -> println "hello ${val}"}
clo("world")
clo.call("world")
所以在闭包中使用递归则用call
直接表示闭包本身,不需要其他额外的语法.
函数支持重载而闭包没有重载这个概念.如下一个简单的方法重载:
def func(arg1, arg2) {
arg1*arg2
}
def func(arg1){
arg1*2
}
println "${func(2, 3)}"
println "${func(4)}"
Groovy中定义的函数重载是参数数量不同函数名称相同,实际上如果函数的形参类型不同也可以重载,比如如下的函数使用了静态的参数类型就可以重载
def func(String arg1) {
arg1*2
}
def func(Number arg1){
arg1*2
}
println "${func(2)}"
println "${func('hello')}"
然而闭包就不能够进行重载
def clo = {
arg -> arg*3
}
def clo = {
arg1,arg2 -> arg1*arg2
}
println "${clo(2)}"
运行报错
The current scope already contains a variable of the name clo
其实这个问题非常好理解, 闭包一般需要与一个闭包变量关联,上述的错误其实就是重复定义变量的错误, 所以闭包里面不存在重载这个说法.
闭包中有四个关键字需要辨别清楚,四个关键字分别是it this owner delegate
其中it
是closure中的隐含参数, 当closure中不明确定义参数列表的时候, 其实可以使用其内部隐藏的参数it
, 比如:
def clo = {
println "hello $it"
}
clo("world")
注意这个it
一定是没有指定参数列表的时候有效, 如果已经指定了参数列表,那么这个it
就无效了, 比如:
def clo = {item ->
println "hello $it"
}
clo("world")
运行出错, 错误是:
groovy.lang.MissingPropertyException: No such property: it for class: groovythis
总结就是当closure最多只需要一个参数的时候,可以不显示定义参数列表,在closure的body里面直接使用隐藏的参数it
即可获取形参的值.
this
关键字:这个关键字和method
中的this的意义是一样的,表示定义这个closure的直接的class的对象实例, 但是由于closure本身比较灵活,容易混淆.
class Enclosing {
void run() {
def clo = {
getThisObject()
// closure 中调用getThisObject方法
// 返回的是封装它的类的实例
}
assert clo() == this
def clo2 = {this} // closure 中
// 的this 关键字依然返回的是定义时包装它的类的实例
// this instanceof Enclosing
assert clo2() == this
}
}
new Enclosing().run()
class EnclosedInnerClass {
class Inner {
def cl = {this}
// 内部类中定义的closure中的this 返回的是
// 内部类的实例 this instanceof EnclosedInnerClass
}
void run() {
def inner = new Inner()
assert inner.cl() == inner
}
}
new EnclosedInnerClass().run()
class NestedClosures {
void run(){
def nestedClosures = {
def cl = {this}
cl()
}
assert nestedClosures() == this
// 嵌套闭包中的this 返回的依然是包装它的类的实例
// 而不是包装它的闭包对象 this依然instanceof NestedClosures
}
}
new NestedClosures().run()
以上例子中包含了所有的closure中this会使用到的情况,总之无论是内部类中的closure, 还是嵌套的closure,其中的this
都是直接包含它的类的实例对象.
owner
关键字:它与this
相似, 区别是 this
指的是类的实例, 而owner
指的是包含定义这个closure的对象, 可以是一个类的实例也可以是一个closure. 也就是说如果这个closure直接定义在一个class里面,那么this
和owner
指的是同一个对象(class的实例). 但是如果这个closure嵌套在另外一个closure里面那么owner
返回的是这个外层的closure对象,而this
返回的是顶层的class对象实例.closure中调用getOwner()
方法可以返回这个owner
. 如下的例子:
class Enclosing {
void run() {
def clo = {
getOwner()
// closure 中使用getOwner 返回owner对象
// 此时是一个class直接包装了closure
// 返回值即是这个class的实例
}
assert clo() == this
def clo2 = {owner}
//此时this 和 owner指代的都是这个class的实例
assert clo2() == this
}
}
new Enclosing().run()
class EnclosedInnerClass {
class Inner {
def cl = {owner}
// 内部类中定义的closure返回的owner 依然是这个内部类的实例
}
void run() {
def inner = new Inner()
assert inner.cl() == inner
}
}
new EnclosedInnerClass().run()
class NestedClosures {
void run(){
def nestedClosures = {
def cl = {owner}
cl()
}
assert nestedClosures() == nestedClosures
// 这是与this最本质的区别 owner可以返回class实例对象或者closure对象,但是是最直接的对象,这个地方显然nestedClosures是直接包含的对象
// 所以owner指代的值是nestedClosures
}
}
new NestedClosures().run()
delegate
关键字: closure中最有意思的一个属性,默认的初始值和owner
一样, 但是delegate
关键字是用户可自定义的, 也就是说可以人为改变这个属性的引用对象,以及在这个属性上面添加额外的用户自定义属性,这个关键字相当于给closure
添加了额外的scope
class Test {
void run(){
def clo1 = {
delegate
}
def clo2 = {
getDelegate()
}
def clo3 = {
owner
}
assert clo1() == clo2()
assert clo1() == clo3()
// closure中使用getDelegate() 方法可以返回这个delegate对象
// 默认的delegate值是owner 所以第二个断言返回为true
def nestedClosure = {
{-> delegate}.call()
// 此时的返回值是外层的nestedClosure 等于owner
}
assert nestedClosure() == nestedClosure
}
}
new Test().run()
resolveStrategy
可以简单理解为closure中的属性访问的策略,是closure中的精华所在,在closure
中涉及到属性的访问的时候可以不明确的指定属性存在的对象,举个例子:
class Test{
def name
}
def clo = {
name.toUpperCase()
}
def test = new Test(name:'Kris')
clo.delegate = test
println clo() // 成功输出KRIS
也就是说当我们在closure
中访问属性的时候是可以需要以属性绑定者.属性名称
的方式访问, 因为closure本身有一个机制就是在所有可能的属性绑定者中查询到需要访问的属性, 默认的查询链即是owner->delegate
也就是说当我们在closure中访问一个属性的时候首先从包装这个closure的类实例中查找,如果没有再从包装这个类的对象(可能是类实例也可能是另外一个closure)中查找,最后从delegate
中查找. 官方提供了五种:1.Closure.OWNER_FIRST
默认的访问策略,先从owner中查找属性, 然后从delegate中查找属性;2.Closure.DELEGATE_FIRST
与上面相反, 先从delegate查找属性,再从owner中查找属性;3.Closure.OWNER_ONLY
只在owner中查找属性,delegate会被忽略;4.Closure.DELEGATE_ONLY
只在delegate中查找属性, owner会被忽略;
5.Closure.TO_SELF
理解为自定义的访问策略,只是在实现Closure子类的时候使用.举个例子:
class Test{
def name
def clo = {
name
}
}
class A {
def name
}
def a = new A(name:"Kris")
def test = new Test(name:"Wu")
test.clo.delegate = a // 如果使用默认的访问优先级为owner所以返回值为Wu
test.clo.resolveStrategy = Closure.DELEGATE_FIRST // 设置优先从delegate访问
assert test.clo() == 'Kris'
当然使用ONLY型的访问策略,如果找不到指定的属性则会抛出groovy.lang.MissingPropertyException
异常, 比如:
class Test{
def name
def age
def clo = {
age
}
}
class A {
def name
}
def a = new A(name:"Xiaoming")
def test = new Test(name:"Wu", age:20)
test.clo.delegate = a
assert test.clo() == 20
test.clo.resolveStrategy = Closure.DELEGATE_ONLY
// age 属性不在delegate里面 所以会抛出属性不存在的异常
try{
test.clo()
}catch(MissingPropertyException ex){
}
Groovy同其他的脚本语言一样是弱类型的语言, 声明变量的时候不需要指明类型, 同样定义函数或者闭包的时候也不需要指明变量的类型.但是有时候为了让调用更加的安全, 也可以指定参数或者变量的类型.
def func1(arg1, arg2) {
arg1*arg2
}
println "${func1(2, 3)}"
println "${func1(3.5, 2)}"
println "${func1('test', 3)}"
运行结果:
6
7.0
testtesttest
一个简单的乘法运算如果不限定参数类型可能会得到一些意想不到不到的结果, 所以当我们的函数或者闭包需要更安全的调用时,还是需要指定参数的类型以及返回值类型
def func2(Number arg1, Number arg2) {
arg1*arg2
}
def clo = {
Number arg1, Number arg2 -> arg1*arg2
}
println "function result is ${func2(1, 2)}"
println "closure result is ${clo(1,2)}"
println "function result is ${func2('test', 2)}"
println "closure result is ${clo('test', 2)}"
后两行的运行会报错, 与预想的一样,输入类型不支持字符串类型
ConsoleScript1.func2() is applicable for argument types: (String, Integer) values: [test, 2]
Possible solutions: func2(java.lang.Number, java.lang.Number)
闭包与函数都支持默认参数, 且默认参数都放置于参数列表的最后
def func(arg1, arg2='world') {
println "${arg1} ${arg2}"
}
def clo = {
arg1, arg2='world' -> println "${arg1} ${arg2}"
}
func('hello')
clo('hello')
func('nihao')
clo('nihao')
运行结果:
hello world
hello world
nihao world
nihao world
本文主要分析了一下Groovy中函数与闭包的相同点与不同点,这些只是在工作和学习过程中的一些比较浅显的总结,如果后续更加深入的了解了Groovy语言以及函数与闭包的其他特点,还会继续更新本文.