原文档链接:https://groovy-lang.org/closures.html

Groovy是一门基于JVM的语言,堪称动态语言版Java,其各种动态语言特性填补了Java的各种空缺,让人拍案叫绝……有幸接触Groovy,遂手动翻译三篇官方文档,以便于读者从Java到Groovy的快速迁移。


本章节主要讲解Groovy的闭包。Groovy中的闭包是一个开放、匿名、可以携带参数的代码块,同时可以返回一个指定变量的值。一个闭包可以引用在它周围声明的变量。与闭包的正式定义相反的,Closure在Groovy语言中还可以包含在其周围范围之外定义的自由变量。虽然打破了闭包的正式概念,但它提供了本章中描述的各种优点。


1. 语法

1.1. 定义一个闭包

一个闭包的定义遵循以下语法:

{ [closureParameters -> ] statements }

[closureParameters->]是一个可选的逗号分隔的参数列表,语句是0个或者多个Groovy语句。这些参数看起来像方法的参数列表,这些参数类型可有可无。

当一个参数列表被指定,->符号是必须的,它能够在闭包体中提供分割参数的功能。语句部分由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) 的闭包

4

it是显式参数的替代版本

5

在这种情况下,通常最好为参数使用显式名称

6

一个接受两个类型参数的闭包

7

一个闭包可以包含多个语句

1.2. 闭包是一个对象

闭包是groovy.lang.Closure类的一个实例,使它可以像任何其他变量一样分配给变量或字段,尽管它是一个代码块:

def listener = { e -> println "Clicked on $e.source" }      (1)
assert listener instanceof Closure
Closure callback = { println 'Done!' }                      (2)
Closure<Boolean> isTextFile = {
    File it -> it.name.endsWith('.txt')                     (3)
}

1

你可以将闭包分配给变量,它是groovy.lang.Closure的一个实例

2

如果不使用defvar,请使用groovy.lang.Closure作为类型

3

或者,你可以使用groovy.lang.Closure的泛型类型指定闭包的返回类型

1.3. 调用一个闭包

一个闭包,作为一个匿名的代码块,可以像其他任何方法一样被调用。如果你定义一个不带任何参数的闭包,像这样:

def code = { 123 }

闭包中的代码只有当你调用的时候才会执行,这可以使用变量来实现,就像它是一个常规方法一样:

assert code() == 123

或者,你可以显式地使用call方法:

assert code.call() == 123

如果闭包接受参数,原则是一样的:

def isOdd = { int i -> i%2 != 0 }                           (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”参数。


2. 参数

2.1. 正常参数

闭包的参数遵循与常规方法的参数相同的原则:

  • 可选类型
  • 一个名字
  • 可选的默认值

参数用逗号分隔:

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) == 3def closureWithTwoArgsAndExplicitTypes = { int a, int b -> a+b }
assert closureWithTwoArgsAndExplicitTypes(1,2) == 3def closureWithTwoArgsAndOptionalTypes = { a, int b -> a+b }
assert closureWithTwoArgsAndOptionalTypes(1,2) == 3def closureWithTwoArgAndDefaultValue = { int a, int b=2 -> a+b }
assert closureWithTwoArgAndDefaultValue(1) == 3

2.2. 隐式参数

当一个闭包没有显示的声明参数列表(使用->),一个闭包总是定义了一个隐式的参数,叫做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)

2.3. Varargs

闭包可以像任何其他方法一样声明变量参数。如果最后一个参数是可变长度(或数组),则Vargs方法是可以接受可变数量参数的方法,如下例所示:

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

接受可变数量的字符串作为第一个参数的闭包

2

可以使用任意数量的参数调用它,而无需将它们显式包装到数组中

3

如果将args参数声明为数组,则可以直接使用相同的行为

4

只要最后一个参数是数组或显式 vargs 类型


3. 委托策略

3.1. Groovy闭包 vs lambda表达式

Groovy将闭包定义为Closure类的实例。这使得它跟lambda expressions in Java 8非常的不一样。委托在Groovy闭包中是一个跟lambda不相同的重要概念。更改委托更改闭包的委托策略的能力使得在 Groovy 中设计漂亮的领域特定语言 (DSL) 成为可能。

3.2. Owner, delegate和this

为了理解delegate的概念,我们首先必须解释下闭包中this的含义。一个闭包实际上定义了3个不同的东西:

  • this对应于定义闭包的封闭类
  • owner对应于定义闭包的封闭对象,可以是类也可以是闭包
  • delegate对应于第三方对象,只要未定义消息的接收者,就会解析方法调用或属性
3.2.1. this的含义

在闭包中,调用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

Enclosure类中定义了一个闭包,并返回getThisObject

2

调用闭包将返回定义闭包的Enclosure实例

3

通常,你只想使用this表示法的快捷方式

4

它返回完全相同的对象

5

如果闭包是在内部类中定义的

6

闭包中的this返回内部类,而不是顶级类

7

在嵌套闭包的情况下,就像这里cl被定义在NestedClosures的范围内

8

那么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'

闭包在this上调用toString,这实际上会在封闭对象上调用toString方法,即Person实例

3.2.2. 闭包的所有者Owner

闭包的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

Enclosure类中定义了一个闭包,并返回getOwner

2

调用闭包将返回定义闭包的Enclosure实例

3

通常,你只想使用快捷方式owener表示法

4

它返回完全相同的对象

5

如果闭包是在内部类中定义的

6

闭包中的owener返回内部类,而不是顶级类

7

但是在nestedClosures的情况下,就像这里cl被定义在嵌套闭包的范围内

8

那么owner对应于封闭的闭包,因此与this不同的对象!

3.2.3. 闭包的委托Delegate

可以使用委托属性或调用getDelegate方法来访问闭包的委托。它是在 Groovy 中构建特定领域语言的强大概念。虽然this和owner指的是闭包的词法范围,但委托是闭包将使用的用户定义对象。默认情况下,委托设置为owener

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方法获取闭包的委托

2

或使用delegate属性

3

两者都返回相同的对象

4

这是封闭类或闭包

5

特别是在嵌套闭包的情况下

6

delegate将对应于owner

闭包的委托可以更改为任何对象。让我们通过创建两个类来说明这一点,它们不是彼此的子类,但都定义了一个名为name的属性:

class Person {
    String name
}
class Thing {
    String name
}def p = new Person(name: 'Norman')
def t = new Thing(name: 'Teapot')

然后让我们定义一个闭包来获取委托上的name属性:

def upperCasedName = { delegate.name.toUpperCase() }

然后通过更改闭包的委托,可以看到目标对象会发生变化:

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。如下一段所述。
3.2.4. 委托策略

每当在闭包中访问一个属性而不显式设置接收器对象时,就会涉及到委托策略:

class Person {
    String name
}
def p = new Person(name:'Igor')
def cl = { name.toUpperCase() }                 (1)
cl.delegate = p                                 (2)
assert cl() == 'IGOR'                           (3)

1

name没有引用闭包词法范围内的变量

2

我们可以将闭包的委托更改为Person的实例

3

并且方法调用会成功

此代码有效的原因是name属性将在delegate对象上透明地解析!这是解决闭包内的属性或方法调用的一种非常强大的方法。无需设置显式delegate。接收者:将进行调用,因为闭包的默认委托策略使其如此。闭包实际上定义了多种解决策略,你可以选择:

  • Closure.OWNER_FIRST默认策略。如果owner上存在属性/方法,那么它将在owner上调用。如果不是,则使用delegate
  • Closure.DELEGATE_FIRST反转逻辑:先使用delegate,然后使用owner
  • Closure.OWNER_ONLY只会解析所有者的属性/方法查找:委托将被忽略。
  • Closure.DELEGATE_ONLY只会解析委托上的属性/方法查找:所有者将被忽略。
  • Closure.TO_SELF可供需要高级元编程技术并希望实现自定义解析策略的开发人员使用:解析不会针对所有者或委托,而只会针对闭包类本身。如果你实现自己的Closure子类,则使用它才有意义。

让我们用这段代码来说明默认的“所有者优先”策略:

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

PersonThing类都定义了一个name属性

3

使用默认策略,首先在所有者上解析name属性

4

因此,如果我们将delegate更改为t,它是Thing的一个实例

5

结果没有变化:name首先在闭包的owner上解析

但是,可以更改闭包的解析策略:

p.pretty.resolveStrategy = Closure.DELEGATE_FIRST
assert p.toString() == 'My name is Teapot'

通过更改resolveStrategy,我们正在修改Groovy解析“隐式 this”引用的方式:在这种情况下,name将首先在委托中查找,如果未找到,则在所有者中查找。由于name是在委托中定义的,它是Thing的一个实例,因此使用此值。

如果其中一个委托人(或所有者)没有这样的方法或属性,则可以说明“委托优先”和“仅委托”或“所有者优先”和“仅所有者”之间的区别:

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
}

在这个例子中,我们定义了两个类,它们都有一个name属性,但只有Person类声明了一个agePerson类还声明了一个引用age的闭包。我们可以将默认的解决策略从“所有者优先”更改为“仅代表”。由于闭包的所有者是Person类,那么我们可以检查,如果委托是Person的实例,则调用闭包是成功的,但是如果我们以委托作为Thing的实例来调用它,它会失败并出现groovy .lang.MissingPropertyException。尽管在Person类中定义了闭包,但没有使用所有者。


4. GStrings中的闭包

采取以下代码:

def x = 1
def gs = "x = ${x}"
assert gs == 'x = 1'

代码的行为与你预期的一样,但是如果你添加以下内容会发生什么:

x = 2
assert gs == 'x = 2'

你会看到断言失败了!有两个原因:

  • GString仅懒惰地执行值的toString表示
  • GString 中的语法${x}代表闭包,而是$x表达式,在创建GString时进行执行。

在我们的示例中,GString是使用引用x的表达式创建的。创建GString时,x为 1,因此创建的GString的值为 1。触发断言时,执行GString并使用toString将 1 转换为String。当我们将x更改为 2 时,我们确实更改了x的值,但它是一个不同的对象,并且GString仍然引用旧的对象。

如果你需要 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类有一个toString方法返回name属性

2

我们创建了一个名为SamPerson

3

我们创建另一个名为LucyPerson

4

p变量设置为Sam

5

并创建一个闭包,引用p的值,也就是说Sam

6

所以当我们评估字符串时,它会返回Sam

7

如果我们将p更改为Lucy

8

该字符串仍然计算为Sam,因为它是创建GStringp

9

所以如果我们把name的值Sam改成Lucy

10

这次GString被正确地改变了

因此,如果你不想依赖变异对象或包装对象,则必须通过显式声明一个空参数列表来在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'


5. 强制闭包

闭包可以转换为接口或单抽象方法类型。有关完整说明,请参阅手册的这一部分


6. 函数式编程

闭包,如Java 8中的lambda表达式,是Groovy中函数式编程范式的核心。函数的一些函数式编程操作可以直接在Closure类上使用,如本节所示。

6.1. 柯里化

在Groovy中,柯里化是指部分应用的概念。由于Groovy在闭包上应用的不同作用域规则,它不符合函数式编程中柯里化的真正概念。 Groovy中的柯里化将允许你设置闭包的一个参数的值,并且它将返回一个接受少一个参数的新闭包。

6.1.1. 左柯里化

左柯里化是设置闭包最左边的参数,如下例所示:

def nCopies = { int n, String str -> str*n }    (1)
def twice = nCopies.curry(2)                    (2)
assert twice('bla') == 'blabla'                 (3)
assert twice('bla') == nCopies(2, 'bla')        (4)

1

nCopies闭包定义了两个参数

2

curry将第一个参数设置为 2 ,创建一个接受单个String的新闭包(函数)

3

所以只用一个String调用新的函数调用

4

并且相当于用两个参数调用nCopies

6.1.2. 右柯里化

与左柯里化类似,可以设置闭包的最右侧参数:

def nCopies = { int n, String str -> str*n }    (1)
def blah = nCopies.rcurry('bla')                (2)
assert blah(2) == 'blabla'                      (3)
assert blah(2) == nCopies(2, 'bla')             (4)

1

nCopies闭包定义了两个参数

2

rcurry会将最后一个参数设置为bla,创建一个接受单个int的新闭包(函数)

3

所以只用一个int调用新的函数调用

4

并且相当于用两个参数调用nCopies

6.1.3. 基于索引的柯里化

如果闭包接受超过 2 个参数,则可以使用ncurry设置任意参数:

def volume = { double l, double w, double h -> l*w*h }      (1)
def fixedWidthVolume = volume.ncurry(1, 2d)                 (2)
assert volume(3d, 2d, 4d) == fixedWidthVolume(3d, 4d)       (3)
def fixedWidthAndHeight = volume.ncurry(1, 2d, 4d)          (4)
assert volume(3d, 2d, 4d) == fixedWidthAndHeight(3d)        (5)

1

volume函数定义了3个参数

2

ncurry将第二个参数 (index = 1) 设置为2d,创建一个接受长度和高度的新体积函数

3

该功能相当于调用volume省略宽度

4

也可以设置多个参数,从指定的索引开始

5

结果函数接受与初始参数一样多的参数减去ncurry设置的参数数量

6.2. 记忆

记忆化允许缓存调用闭包的结果。如果一个函数(闭包)完成的计算很慢,这很有趣,但你知道这个函数会经常用相同的参数调用。一个典型的例子是斐波那契套件。一个简单的实现可能如下所示:

def fib
fib = { long n -> n<2?n:fib(n-1)+fib(n-2) }
assert fib(15) == 610 // slow!

这是一个幼稚的实现,因为“fib”通常使用相同的参数递归调用,从而导致指数算法:

  • 计算fib(15)需要fib(14)fib(13)的结果
  • 计算fib(14)需要fib(13)fib(12)的结果

由于调用是递归的,你已经可以看到我们将一次又一次地计算相同的值,尽管它们可以被缓存。这个幼稚的实现可以通过使用memoize缓存调用结果来“修复”:

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

缓存使用参数的实际值工作。这意味着,如果你将memoization与原始类型或盒装原始类型以外的其他内容一起使用,你应该非常小心。

可以使用其他方法调整缓存的行为:

  • memoizeAtMost将生成一个新的闭包,最多缓存n个值
  • memoizeAtLeast将生成一个缓存至少n个值的新闭包
  • memoizeBetween将生成一个新的闭包,它缓存至少n个值和最多n个值

所有memoize变体中使用的缓存都是LRU缓存。

6.3. 组合

闭包组合对应于函数组合的概念,即通过组合两个或多个函数(链式调用)来创建一个新函数,如下例所示:

def plus2  = { it + 2 }
def times3 = { it * 3 }def times3plus2 = plus2 << times3
assert times3plus2(3) == 11
assert times3plus2(4) == plus2(times3(4))def plus2times3 = times3 << plus2
assert plus2times3(3) == 15
assert plus2times3(5) == times3(plus2(5))// reverse composition
assert times3plus2(3) == (times3 >> plus2)(3)

6.4. 蹦床Trampoline

递归算法通常受到物理限制的限制:最大堆栈高度。例如,如果你调用一个递归调用自身太深的方法,你最终会收到一个StackOverflowException

在这些情况下有帮助的一种方法是使用Closure及其trampoline功能。

闭包被包裹在TrampolineClosure中。调用时,蹦床Closure将调用原始Closure等待其结果。如果调用的结果是TrampolineClosure的另一个实例,可能是作为调用trampoline()方法的结果而创建的,则将再次调用Closure。返回的蹦床闭包实例的这种重复调用将继续,直到返回蹦床Closure以外的值。该值将成为蹦床的最终结果。这样,调用是连续进行的,而不是填充堆栈。

下面是使用trampoline()实现阶乘函数的示例:

def factorial
factorial = { int n, def accu = 1G ->
    if (n < 2) return accu
    factorial.trampoline(n - 1, n * accu)
}
factorial = factorial.trampoline()
assert factorial(1)    == 1
assert factorial(3)    == 1 * 2 * 3
assert factorial(1000) // == 402387260.. plus another 2560 digits

6.5. 方法指针

能够使用常规方法作为闭包通常很实用。例如,你可能想要使用闭包的柯里化功能,但这些功能不适用于普通方法。在 Groovy 中,你可以使用方法指针运算符从任何方法中获取闭包。