闭包

本章涵盖 Groovy 闭包。Groovy 中的闭包是一个开放的、匿名的代码块,它可以接受参数、返回值并被赋值给变量。闭包可以引用其周围范围中声明的变量。与闭包的正式定义相反,Groovy 语言中的 Closure 也可以包含在周围范围之外定义的自由变量。虽然这打破了闭包的正式概念,但它提供了一些在本部分中描述的各种优势。

1. 语法

1.1. 定义闭包

闭包定义遵循以下语法

{ [closureParameters -> ] statements }

其中 [closureParameters->] 是可选的逗号分隔的参数列表,而 statements 是 0 个或多个 Groovy 语句。参数看起来类似于方法参数列表,这些参数可以是类型化的或非类型化的。

当指定参数列表时,需要 -> 字符,它用于将参数与闭包主体分隔开。statements 部分由 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) == 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

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. 可变参数

闭包可以像任何其他方法一样声明可变参数。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 闭包与 lambda 表达式

Groovy 将闭包定义为Closure 类的实例。这使其与Java 8 中的 lambda 表达式有很大不同。委托是 Groovy 闭包中的一个关键概念,在 lambda 中没有等效的概念。能够更改委托更改闭包的委托策略使得能够在 Groovy 中设计出漂亮的特定领域语言 (DSL)。

3.2. 所有者、委托和 this

为了理解委托的概念,我们首先需要解释 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 闭包在 Enclosing 类内部定义,并返回 getThisObject
2 调用闭包将返回定义闭包的 Enclosing 的实例
3 通常,您只需使用快捷方式 this 符号
4 它返回完全相同的对象
5 如果闭包在内部类中定义
6 闭包中的 this返回内部类,而不是顶层类
7 对于嵌套闭包,例如 clnestedClosures 的作用域内定义的情况
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'
1 闭包在 this 上调用 toString,它实际上将调用封闭对象上的 toString 方法,即 Person 实例

3.2.2. 闭包的所有者

闭包的所有者与闭包中的 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 闭包在 Enclosing 类内部定义,并返回 getOwner
2 调用闭包将返回定义闭包的 Enclosing 的实例
3 通常,您只需使用快捷方式 owner 符号
4 它返回完全相同的对象
5 如果闭包在内部类中定义
6 闭包中的 owner返回内部类,而不是顶层类
7 但在嵌套闭包的情况下,例如 clnestedClosures 的作用域内定义
8 那么 owner 对应于封闭的闭包,因此它与 this 是不同的对象!

3.2.3. 闭包的委托

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

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是**默认策略**。如果属性/方法存在于**所有者**上,那么它将在所有者上调用。如果没有,则使用**委托人**。

  • Closure.DELEGATE_FIRST反转逻辑:**委托人**首先使用,然后是**所有者**。

  • 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          (1)
cl.delegate = t
assert cl() == 42          (1)

cl.resolveStrategy = Closure.DELEGATE_ONLY
cl.delegate = p
assert cl() == 42          (2)
cl.delegate = t
try {
    cl()                   (3)
    assert false
} catch (MissingPropertyException ex) {
    // "age" is not defined on the delegate
}
1 对于“所有者优先”,委托人是什么并不重要。
2 对于“委托人仅”,将p作为委托人可以成功。
3 对于“委托人仅”,将t作为委托人会失败。

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

有关如何使用此功能开发DSL的完整说明,请参见手册的专用部分

3.2.5. 元编程中的委托策略

在描述“所有者优先”委托策略时,我们讨论了如果所有者“存在”,则使用所有者中的属性/方法,否则使用委托人中的相应属性/方法。类似地,对于“委托人优先”,但顺序相反。使用“存在”这个词可能并不准确,使用“处理”这个词更为准确。这意味着对于“所有者优先”,如果属性/方法存在于所有者中,或者它具有propertyMissing/methodMissing挂钩,那么所有者将处理成员访问。

我们可以看到这一点,我们的先前示例略微修改后,就可以看到这一点。

class Person {
    String name
    int age
    def fetchAge = { age }
}
class Thing {
    String name
    def propertyMissing(String name) { -1 }
}

def p = new Person(name:'Jessica', age:42)
def t = new Thing(name:'Printer')
def cl = p.fetchAge
cl.resolveStrategy = Closure.DELEGATE_FIRST
cl.delegate = p
assert cl() == 42
cl.delegate = t
assert cl() == -1

在此示例中,即使我们的Thing类实例(我们上次使用cl的委托人)没有age属性,但它通过其propertyMissing挂钩处理缺失属性,意味着age将为-1

4. GString中的闭包

请看以下代码。

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

代码的行为符合预期,但是,如果您添加以下内容会发生什么?

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

您会看到断言失败!这是两个原因。

  • GString仅延迟计算值的toString表示。

  • GString中的${x}语法**不**表示闭包,而是表示在创建GString时计算的$x的**表达式**。

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

GString只有在它引用的值发生变异时才会更改其toString表示。如果引用发生变化,则不会发生任何事情。

如果您需要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类具有一个返回name属性的toString方法。
2 我们创建了一个名为Sam的第一个Person
3 我们创建了另一个名为LucyPerson
4 p变量被设置为Sam
5 并创建了一个闭包,引用p的值,即Sam
6 因此,当我们计算字符串时,它返回Sam
7 如果我们将p更改为Lucy
8 字符串仍然计算为Sam,因为它是在创建GStringp的**值**。
9 因此,如果我们修改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. 基于索引的柯里化

如果闭包接受两个以上的参数,则可以使用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将第二个参数(索引 = 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!
缓存**使用参数的实际值**工作。这意味着如果您将记忆化与除原始类型或包装原始类型以外的其他内容一起使用,则应格外小心。

可以使用备用方法调整缓存的行为。

  • memoizeAtMost将生成一个最多缓存n个值的闭包。

  • memoizeAtLeast将生成一个至少缓存n个值的闭包。

  • memoizeBetween将生成一个至少缓存n个值且最多缓存n个值的闭包。

所有记忆化变体中使用的缓存都是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. 蹦床

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

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

闭包被包装在一个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 中,您可以使用方法指针运算符从任何方法获取闭包。