Groovy 的闭包

4,881 阅读15分钟

以下内容大部分来自 Groovy 官方文档,英文能力 OK 的同学建议直接查看官方文档。 相关 Groovy 示例已经上传到 zjxstar 的 GitHub

前言

在 Java 中,通常以创建匿名内部类的方式来定义用于注册事件处理器的方法参数,但这会使得代码变得非常冗长。而 Groovy 中的闭包可以去掉这种冗长感。闭包是轻量级的、短小的、简洁的,是 Groovy 中最重要、最强大的特性之一。

Groovy 的闭包 ( Closure ) 是一个开放的匿名代码块。闭包也是对象,它可以传递参数,可以返回值并分配给变量。Groovy 中的闭包甚至可以打破闭包的正式概念,即:可以包含其作用域外的自由变量。

闭包带来的好处

Groovy 中的闭包避免了代码的冗长,可以辅助创建轻量级、可复用的代码片段。下面举个简单的例子带大家体验一下闭包的便捷性。

例子:求 1 到 n 之间的所有奇数的和。

传统实现中,我们定义一个 sum 方法,其中使用 for 循环实现奇数的累加,代码如下:

def sum(n) {
    total = 0
    for (int i = 1; i <= n; i += 2) {
        // 计算1到n的奇数和
        total += i
    }
    total
}

println(sum(9)) // n等于9

如果改成计算所有奇数的乘积呢?那又得定义一个 multiply 方法,还是使用 for 循环实现,代码如下:

def multiply(n) {
    total = 1
    for (int i = 1; i <= n; i += 2) {
        // 计算1到n的奇数乘积
        total *= i
    }
    total
}

如果再换成求奇数的平方和呢?实现代码基本与 sum 和 multiply 一致,重复的 for 循环,不同的计算方式而已。

仅仅是三种情况就定义了三个方法,而且存在大量重复代码,这种实现太不优雅了。那使用闭包实现呢?我们只需要定义一个高阶函数,传入一个 Closure 参数即可。

def pickOdd(n, closure) { // 此处的closure可以换成别的参数名
    for (int i = 1; i <= n; i += 2) {
        // 执行逻辑都由传入的闭包决定
        closure(i)
    }
}

Groovy 的闭包可以附在一个方法上或者赋值给一个变量。示例中变量 closure 就保持了一个指向闭包的引用。使用 pickOdd 方法的时候传入包含不同逻辑的闭包方法块即可。

// 打印奇数
pickOdd(9) {
    println it // 下文会详细介绍it
}

// 求和
total = 0
pickOdd(9, {
    num -> total += num // 可以访问total变量
})
println("Sum of odd is: ${total}")

显然,通过闭包实现不仅在语法上比传统方式更优雅,还为函数将部分实现逻辑委托出去提供了一种简单、方便的方式。

定义和使用闭包

前一节中简单实现了两个闭包。直观上,所谓的闭包就是使用大括号包裹的代码块。本节会详细介绍 Groovy 中闭包的定义语法以及使用方式。

定义闭包

闭包的定义语法很简单(有点类似 Java 8 中的 Lambda 表达式):

{ [closureParameters -> ] statements }

语法中,[ closureParameters -> ] 代表参数列表,参数可以是 0 个或多个,且是否声明参数类型是可选的。如果声明了参数列表,则 -> 是必须存在的,用以区分参数和执行语句。statements 代表执行语句,一般情况下都有至少一条执行逻辑(可以没有)。当然,你也可以写一个空的闭包 { } ,但这没什么意义。

以下是一些合法的闭包示例:

{} // 接收一个参数,空执行语句

{ ++it } // 接收一个参数,默认it

{ -> println('no param')} // 没有参数,必须写->

// 可以声明参数类型
{ String x, int y -> println("x is ${x}, y is ${y}") }

// 可以省略参数类型
{ x, y -> println("x is ${x}, y is ${y}") }

// 一个参数,多行执行语句
{ x ->
    int y = -x
    println("-x is ${y}")
}

前文提到过闭包也是对象,没错,它是 groovy.lang.Closure 类的实例。所以我们可以将其赋值给变量。

def closure4 = { x, y -> println("x is ${x}, y is ${y}") } // 省略类型
Closure closure6 = { println('Done') } // 声明类型Closure
Closure<Boolean> closure7 = { int a, int b -> a == b } // 可以定义返回类型

使用闭包

闭包的使用非常简单,我们可以像方法一样调用一个闭包,也可以显式地通过 call 方法调用。

def isOdd = { int i -> i%2 != 0 }
assert isOdd(3) == true // 类似方法调用
assert isOdd.call(2) == false // 使用call方法

闭包在用法上与方法有些类似,但还是有区别的。闭包被执行时总是有返回值的,默认情况下会返回 null

Closure closure6 = { println('Done') }
assert closure6() == null // 返回null

def code = { 123 } // 显式返回123
assert code() == 123

向闭包传递参数

定义闭包时可以设置参数,调用时可以和调用普通方法一样传递参数。闭包中的参数分为三个类型:一般参数、隐藏参数和变长参数,下面详细介绍这三类参数。

一般参数

一般参数的设置原则和 Groovy 中方法设置参数的原则保持一致:

  • 参数类型是可选的;

  • 必须有参数名;

  • 参数可以有默认值,即默认参数;

  • 多个参数之间以逗号分隔;

举例说明:

// 一个参数,不声明参数类型
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 关键字,没错,它就是 Groovy 闭包提供的隐藏参数。当闭包没有显示定义参数列表时( 隐藏 -> 符号时 ),闭包会默认提供一个名为 it 的参数,可以在执行语句中使用该参数。此时,这个闭包接受一个参数的传递。

// greeting1 和 greeting2 是等价的
def greeting1 = { "Hello, $it!" }
assert greeting1('Groovy') == 'Hello, Groovy!'

def greeting2 = { it -> "Hello, $it!" }
assert greeting2('Groovy') == 'Hello, Groovy!'

如果希望闭包不接受任何参数,则必须显式地声明 -> 符号。

def magicNumber = { -> 42 }
println(magicNumber())
//magicNumber(11) // 错误,不接受参数

变长参数

我们在 Java 和 Groovy 的方法中都使用过变长参数,只需在参数类型后面添加 ... 符号即可表示变长参数。Groovy 的闭包中也是同样的用法,直接看例子。

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'

闭包的委托策略

闭包虽然是从 Lambda 表达式演变而来,但与之最大的不同是闭包有委托的概念。闭包中修改委托对象和委托策略的能力,能够让它在 DSLs ( Domain Specific Languages ) 领域大放异彩。

要充分理解闭包的委托功能,就不得不先学习它的三个属性了:this、owner 和 delegate。

this

this 对应了定义闭包的所在封闭类。

在闭包中,可以通过 getThisObject 方法获取定义闭包的封闭类,这和显式调用 this 是等价的。

// 普通类
class Enclosing {
    void run() {
        // getThisObject指向Enclosing
        def whatIsThisObject = { getThisObject() }
        assert whatIsThisObject() == this
        println('getThisObject(): ' + whatIsThisObject())
        
        // this指向Enclosing
        // this和getThisObject是等价的
        def whatIsThis = { this }
        assert whatIsThis() == this
        println('this in Closure: ' + whatIsThis())
        println('this: ' + this)
    }
}

// 内部类
class EnclosedInInnerClass {
    class Inner {
        // 此处的this指向Inner,而不是EnclosedInInnerClass
        Closure cl = { this }
    }
    void run() {
        def inner = new Inner()
        assert inner.cl() == inner
        println('Closure in Inner, this: ' + inner.cl())
    }
}

// 嵌套闭包
class NestedClosures {
    // this始终指向NestedClosures
    void run() {
        def nestedClosures = {
            def cl = { this }
            println("cl this: " + cl())
            cl()
        }
        assert nestedClosures() == this
        println("nestedClosures this: " + nestedClosures())
        println("this: " + nestedClosures())
    }
}

从示例中可以看出,this 始终指向着定义闭包的封闭类,也就是这个封闭类的一个实例。

owner

owner 对应了定义闭包的封闭对象,可以是类或者闭包。它与 this 有点类似,最大的不同是 owner 指向的是直接包裹它闭包的对象,这就比 this 多了一种可能——闭包对象。

// 普通类
class EnclosingOwner {
    void run() {
        // getOwner指向EnclosingOwner
        def whatIsOwnerMethod = { getOwner() }
        assert whatIsOwnerMethod() == this
        // owner指向EnclosingOwner
        def whatIsOwner = { owner }
        assert whatIsOwner() == this

        println('getOwner: ' + whatIsOwnerMethod())
        println('owner: ' + whatIsOwner())
        println('this: ' + this)
    }
}

// 内部类
class EnclosedInInnerClassOwner {
    class Inner {
        // 指向Inner
        Closure cl = { owner }
    }
    void run() {
        def inner = new Inner()
        assert inner.cl() == inner
        println('inner owner: ' + inner.cl())
    }
}

// 嵌套闭包
class NestedClosuresOwner {
    void run() {
        def nestedClosures = {
            // 指向的就是nestedClosures闭包对象了,而不是NestedClosuresOwner
            // 这里就和this不一样了
            def cl = { owner }
            println('Closure owner: ' + cl())
            println('Closure this: ' + this)
            cl()
        }
        assert nestedClosures() == nestedClosures
    }
}

delegate

delegate 可以对应任何对象。this 和 owner 都属于闭包的词法范围,而 delegate 指向的是闭包所使用的用户自定义对象。默认情况下,delegate 被设置成 owner 。我们可以通过 delegate 关键字和 getDelegate 方法来使用委托对象。

class EnclosingDelegate {
    void run() {
        // 指向的是EnclosingDelegate ,与this、owner表现一致
        def cl = { getDelegate() }
        def cl2 = { delegate }
        assert cl() == cl2()
        assert cl() == this
        println(cl())

        // 指向的是enclosed ,与owner表现一致
        def enclosed = {
            { -> delegate }.call()
        }
        assert enclosed() == enclosed
        println(enclosed())
    }
}

delegate 可以被修改指向任意对象。

class Person {
    String name
}
class Thing {
    String name
}

def p = new Person(name: 'Tom')
def t = new Thing(name: 'Teapot')

def upperCasedName = { delegate.name.toUpperCase() }
// upperCasedName() // 直接报错,因为delegate指向的对象没有name属性
upperCasedName.delegate = p
println(upperCasedName()) // delegate指向Person
upperCasedName.delegate = t
println(upperCasedName()) // delegate指向Thing

策略调整

闭包的委托是透明的,即使我们没有显式地使用 delegate. 它也是可以正常工作的。例如:

//name = 'jerry' // 如果定义了一个name,下面的闭包会打印JERRY
// 不显式使用 delegate.
def upperCasedName2 = {
    println(name.toUpperCase()) // 打印TOM
    // 返回owner对象
    owner
}
// 直接赋值delegate
upperCasedName2.delegate = p
// 正常工作
println(upperCasedName2()) // 打印owner对象,指向的是groovy脚本
println(upperCasedName2() instanceof GroovyObject) // 其实是GroovyObject类实例

这里 name 属性之所以能被正确的解析并执行都归功于 delegate 对象,因为闭包优先使用 owner 对象来查找 name 属性,发现没有该属性。然后再使用 delegate 对象,此时它指向了 p 拥有 name 属性,所以方法得到执行。

实际上,闭包的委托策略有多个:

  • Closure.OWNER_FIRST :默认策略。如果 owner 对象中有需要的属性或方法,则优先执行;否则将使用 delegate 对象。在上例中,如果在闭包外面(脚本中)声明了一个 name 属性,则闭包中执行的 name 就是该属性了,而轮不到 delegate 指向的对象。

  • Closure.DELEGATE_FIRST :delegate 优先策略,和默认策略逻辑相反,优先找 delegate 而不是 owner。

  • Closure.OWNER_ONLY :只使用 owner 策略,会忽略 delegate。

  • Closure.DELEGATE_ONLY :只使用 delegate 策略,会忽略 owner。

  • Closure.TO_SELF :可供需要高级元编程技术并希望实现自定义解析策略的开发人员使用。解决方案不会在 owner 或 delegate 上进行,而只能在封闭类本身上进行。 只有当你实现自己的 Closure 子类时,这个策略才是有意义的。

那么,怎么调整委托策略呢?只需要赋值闭包的 resolveStrategy 属性即可。

class Person2 {
    String name
    int age
    def fetchAge = { age }
}
class Thing2 {
    String name
}

def p2 = new Person2(name:'Jessica', age:42)
def t2 = new Thing2(name:'Printer')
def cl = p2.fetchAge
cl.delegate = p2
assert cl() == 42 // owner里有age
cl.delegate = t2
assert cl() == 42 // 还是找的owner,因为默认策略是owner优先
cl.resolveStrategy = Closure.DELEGATE_ONLY //切换为只使用delegate
cl.delegate = p2 
assert cl() == 42 // delegate指向p2,有age属性
cl.delegate = t2 
try {
    cl() // delegate指向t2,但t2中没有age属性,会抛出异常
    assert false
} catch (MissingPropertyException ex) {
    // "age" is not defined on the delegate
}

动态闭包

所谓动态闭包就是在代码逻辑中判断传入的 Closure 是否满足某些要求,比如:Closure 是否存在,Closure 的参数数量和类型是否符合要求等,从而增加代码的灵活性。

举例说明:

判断方法执行时是否传入了闭包参数。如果传了,则执行闭包逻辑;否则执行默认逻辑。

def say(closure) {
    if (closure) { // 判断是否传入了一个闭包
        closure()
    } else {
        println('say nothing')
    }
}

say()
say() {
    println('i\'m hungry')
}

判断传入的闭包的参数个数是否为 2 个。

def compute(amount, Closure computer) {
    total = 0
    // 判断闭包的参数个数是否为2
    if (computer.maximumNumberOfParameters == 2) {
        total = computer(amount, 6)
    } else {
        total = computer(amount)
    }

    println("total is $total")
}

compute(100) {
    it * 8 // 800
}

compute(100) { // 600
    amount, weight ->
        amount * weight
}

我们也可以对闭包的参数类型做要求。

def execute(Closure closure) {
    println("params count: ${closure.maximumNumberOfParameters}")

    // 判断delegate
    if (closure.delegate != closure.owner) {
        println('I have a custom delegate')
    }

    // 遍历闭包的参数类型
    for (paramType in closure.parameterTypes) {
        if (paramType.name == 'java.util.Date') {
            println('Force today')
        } else {
            println(paramType.name)
        }

    }
}

execute {} // 一个Object类型参数
execute { it } // 一个默认参数,Object类型
execute { -> } // 没有参数
execute { String value -> println(value) } // 一个String类型参数
execute {int a, Date b -> println('Two params')} // 两个参数,int类型和Date类型

class A {
    String name
}
Closure closure = { println(it) }
closure.delegate = new A(name: 'Tom')
execute(closure)

动态的检查闭包就可以让方法逻辑更加丰富,更加灵活了。

闭包的特色用法

前文中详细介绍了 Groovy 闭包的基本概念、定义方式、基本使用以及重要的委托策略等,已经初步体现了闭包的强大。闭包作为 Groovy 最重要的特色之一,它还有哪些有意思的特性呢?

在 GStrings 中使用

前面的例子中,在字符串里用到了很多带括号的写法,比如:"My name is ${name}" ,这里面是闭包嘛?我们先来看一个例子。

def x = 1
// 这里的 ${x} 并不是闭包,而是一个表达式
def gs = "x = ${x}" // gs的值已经确定了
assert gs == 'x = 1'

x = 2
//assert gs == 'x = 2' // gs还是 x = 1
println(gs)

很遗憾,这种写法并不是闭包,而只是一个表达式:$x,它的值在 GString 创建时就已经确定了。所以,当 x 赋值为 2 时,gs 的值并没有改变。

那如何实现 GString 中闭包的懒加载呢?就必须使用 ${-> x} 这种格式了,即显式声明 -> ,这样在 GString 里才会是一个闭包。

def y = 1
// 这里的 ${-> y} 才是一个闭包
def gsy = "y = ${-> y}"
assert gsy == 'y = 1'

y = 2
assert gsy == 'y = 2' // 会重新使用y值, y = 2

其实,GString 只会延迟计算 toString 的表示,即只针对值的变化,而无法作用于引用的变化。

// 定义一个类,包含toString方法
class Dog {
    String name
    String toString() { name }
}
def sam = new Dog(name:'Sam')
def lucy = new Dog(name:'Lucy')
def p = sam // 给p赋值
def gsDog = "Name: ${p}" // 此时gsDog的值已经确定了
assert gsDog == 'Name: Sam'
p = lucy // 虽然p的引用发生了变化,但对gsDog无效
assert gsDog == 'Name: Sam'
sam.name = 'Lucy' // name的值发生了变化,会影响gsDog的值
assert gsDog == 'Name: Lucy'

// 如果改成闭包,就会针对引用有效
def sam2 = new Dog(name: 'Sam2')
def lucy2 = new Dog(name: 'Lucy2')
def q = sam2
def gsDog2 = "Name: ${-> q}" // 使用闭包
assert gsDog2 == 'Name: Sam2'
q = lucy2 // q的引用发生了变化,对gsDog依然有效
assert gsDog2 == 'Name: Lucy2'

闭包的强转

Groovy 中闭包是可以强转成 SAM 类型的。所谓的 SAM 类型是只包含一个抽象方法的接口或者抽象类。我们可以通过 as 关键字实现强转。

// SAM类型的接口和抽象类
interface Predicate<T> {
    boolean accept(T obj)
}

abstract class Greeter {
    abstract String getName()
    void greet() {
        println "Hello, $name"
    }
}

// 使用as关键字强转
Predicate filter = { it.contains 'G' } as Predicate
assert filter.accept('Groovy') == true

Greeter greeter = { 'Groovy' } as Greeter
greeter.greet()
println(greeter.getName())

// 2.2.0之后的版本可以省略as关键字
Predicate filter2 = { it.contains 'G' }
assert filter2.accept('Groovy') == true

Greeter greeter2 = { 'Groovy' }
greeter2.greet()

Groovy 的 2.2.0 版本以上可以省略 as 关键字。

其实,除了 SAM 类型之外,闭包是可以强制转换为任意类型的,特别是接口。

// 闭包可以强制转成任意类,特别是接口
//interface FooBar {
//    int foo()
//    void bar()
//}

class FooBar {
    int foo() { 1 }
    void bar() { println 'barn' }
}

def impl = { println 'ok'; 123 } as FooBar

assert impl.foo() == 123 // true
impl.bar() // 打印 ok

Curring

Curring 的中文是柯里化,属于函数式编程的一种概念。但 Groovy 中的 Curring 并不符合真正的 Curring 概念,这是因为 Groovy 中的闭包有着不同的作用域规则。在 Groovy 中使用 Curring 操作可以给 Closure 的参数列表中其中一个或多个参数进行赋值,然后会返回一个新的 Closure。

def nCopies = { int n, String str -> str*n }
def twice = nCopies.curry(2) // 最左边的参数赋值
assert twice('bla') == 'blabla'
assert twice('bla') == nCopies(2, 'bla')
def blah = nCopies.rcurry('bla') // 最右边的参数赋值
assert blah(2) == 'blabla'
assert blah(2) == nCopies(2, 'bla')

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)

def oneCurring = { item -> println(item)} // 支持一个参数的情况
def oneCurring2 = oneCurring.curry("hello")
oneCurring2()

def funCurring = {int a, int b, int c, int d -> a * b * c * d}
def funCurring2 = funCurring.ncurry(1, 2, 3)
assert funCurring(1, 2, 3, 4) == funCurring2(1, 4)

Memoization

Groovy 的闭包拥有记忆化的能力,可以理解为缓存调用闭包的返回结果。这个特性能给一些需要进行重复计算的递归算法提供帮助,典型的例子是斐波那契数列。

def fib
fib = { long n -> n < 2 ? n : fib(n - 1) + fib(n - 2) }
// 某些值会被重复计算
assert fib(15) == 610 // slow!

计算 fib(15) 时会要求先计算 fib(14) 和 fib(13) ,计算 fib(14) 时,又要先计算 fib(13) 和 fib(12) 。显然 fib(13) 被重复计算了。使用闭包的 memoize 方法就可以避免这些重复计算。

fib = { long n -> n < 2 ? n : fib(n - 1) + fib(n - 2) }.memoize()
assert fib(25) == 75025 // fast!

除了 memoize 方法外,闭包还提供了几个方法可以控制缓存的数量,这些方法使用的是 LRU 策略。

  • memoizeAtMost :可以设置最多缓存 n 个值,会生成一个新的 Closure;
  • memoizeAtLeast :可以设置最少缓存 n 个值,会生成一个新的 Closure;
  • memoizeBetween :可以设置缓存的数量范围,会生成一个新的 Closure;
fib = { long n -> n < 2 ? n : fib(n - 1) + fib(n - 2) }.memoizeAtMost(8)
assert fib(25) == 75025 // fast!
fib = { long n -> n < 2 ? n : fib(n - 1) + fib(n - 2) }.memoizeAtLeast(3)
assert fib(25) == 75025 // fast!
fib = { long n -> n < 2 ? n : fib(n - 1) + fib(n - 2) }.memoizeBetween(3, 8)
assert fib(25) == 75025 // fast!

需要注意的是,闭包的缓存是使用参数的实际值工作的。这意味着如果是使用除基本类型或者包裹类型之外的其他内容进行记忆化,就得额外小心了。

Trampoline

Trampoline 特性是专门为递归调用提供的。使用递归时,最大的隐患是堆栈限制,如果递归深度过深,容易引发 StackOverflowException 异常。闭包的 Trampoline 特性能够帮助递归方法规避这个异常(尾递归)。

当我们使用 trampoline() 方法时,会将当前的 Closure 包装成 TrampolineClosure 。调用时就会返回 TrampolineClosure 的实例,并调用 call 方法,原闭包的逻辑就会被执行。如果有递归调用,则 call 方法返回的是 TrampolineClosure 另一个实例,从而又会触发原闭包的逻辑。以此不断循环,直到返回非 TrampolineClosure 的值才会结束。这样就将原来的递归堆栈调用转换成了方法的连续调用,从而避免堆栈溢出。

def factorial
factorial = { int n, def accu = 1G ->
    if (n < 2) return accu
    factorial.trampoline(n - 1, n * accu)
}
factorial = factorial.trampoline() // 会转成 TrampolineClosure

assert factorial(1) == 1
assert factorial(3) == 1 * 2 * 3
assert factorial(1000) // == 402387260.. plus another 2560 digits

Composition

闭包的组合相对其他特性来说简单明了,可以理解为方法的组合调用。比如:有函数 f 和 g ,可以构成一个新的函数 h = f( g( ) ) 或者 h = g( f( ) ) 等。举个例子就明白了:

def plus2  = { it + 2 }
def times3 = { it * 3 }

// 先计算箭头的尾部闭包,再将其值传入箭头的头部闭包中进行计算
def times3plus2 = plus2 << times3 // plus2(times3())
assert times3plus2(3) == 11
assert times3plus2(4) == plus2(times3(4))

def plus2times3 = times3 << plus2 // times3(plus2())
assert plus2times3(3) == 15
assert plus2times3(5) == times3(plus2(5))

// reverse composition
def times3plus22 = times3 >> plus2
assert times3plus2(3) == times3plus22(3) // plus2(times3())
println(times3plus22(2)) // 8

总结

本文介绍了 Groovy 中的闭包,详细说明了闭包的定义规则、委托机制以及特色使用。这些内容已经体现出了闭包的魅力,但远远不只如此。闭包真正强大的地方是它在 DSL 领域的作用。有兴趣的同学可以自行预习,在后续的 Gradle 文章中会用到 DSL 。

参考资料

  1. 柯里化函数
  2. 真正意义上的闭包
  3. Groovy 官方网站
  4. 尾递归
  5. LRU 算法
  6. Lambda 表达式
  7. 《Groovy程序设计》