Gradle探索之旅(五)——Groovy闭包

闭包是Groovy的精髓,核心概念之一。它是构建gradle DSL的必备特性,我们将通过一整个小结来学习它。

目录

1. 闭包的基础语法

1.1 定义闭包

1.2 调用闭包

1.3 闭包参数

1.3.1 普通参数

1.3.2 隐式参数

1.3.3 可变参数

1.4 委托

1.4.1 this的含义

1.4.2 Owner的含义

1.4.3 Delegate的含义

1.4.4 委托策略

1.5 柯里化

2. 闭包的本质

3. 闭包的优点及使用场景

4. 结语

5. 参考


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。为了理解委托,我们首先来认识下闭包中的三个关键字:

  1. this
  2. owner
  3. 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方法来获取闭包的委托。它是一个非常强大的特性,通过它来构建DSLthis和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.;属性仍然可以正确被访问,这得益于闭包的委托策略。闭包提供了多种策略可以选择:

  1. OWNER_FIRST 这是默认策略。如果属性和方法存在于owner中,则调用owner的属性和方法。如果没找到,会调用delegate对应的属性和方法。
  2. DELEGATE_FIRST 与OWNER_FIRST逻辑刚好相反,会优先使用delegate,然后再考虑owner。
  3. OWNER_ONLY 只会去解析owner中的属性和方法。delegate会被忽略。
  4. DELEGATE_ONLY 只会解析delegate中的属性和方法。owner会被忽略。
  5. 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. 参考

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值