概述
目录
1. 闭包
1.1 闭包的语法
定义闭包
闭包作为对象使用
调用闭包
1.2 闭包参数
常规参数
隐式参数
变长参数
1.3 闭包的委派策略
Groovy 闭包和 lambada 表达式
闭包的 owner, delegate 和 this
1.4 GString 中的闭包
1.5 函数式编程
科里化 Currying
记忆化 Memoization
闭包合成 Composition
串行调用 Trampoline
方法指针
1. 闭包
Groovy 闭包是一个可以接受参数的匿名代码块,它可以返回一个值,也能被赋值给一个变量。闭包可以引用那些声明在它周围作用域中的变量。和经典的闭包定义不同,Groovy 里面的闭包 (Closure)还可以包含定义在它周围作用域之外的自由变量。尽管这违背了经典的闭包概念,但是这却带来一大堆有点,下文中我们会细谈。
1.1 闭包的语法
定义闭包
闭包定义使用下面的语法:
{ [closureParameters -> ] statements }
[closureParameters -> ] 表示一个由逗号分割的可选的参数列表,statements 表示 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: 包含多条语句的闭包
闭包作为对象使用
闭包是 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: 如果不实用 def, 可以把闭包赋值给 groovy.lang.Closure 类型的变量
//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 参数。
1.2 闭包参数
常规参数
闭包参数和正常方法的参数都遵循相同的规则:
- 一个可选的类型声明
- 一个参数名称
- 一个可选的默认值
参数是使用逗号分割的:
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 的隐式的参数。这意味着下面这些代码:
def greeting = { "Hello, $it!" }
assert greeting('Patrick') == 'Hello, Patrick!'
和下面这些是完全等效的:
def greeting = { it -> "Hello, $it!" }
assert greeting('Patrick') == 'Hello, Patrick!'
因此,如果你想定义一个不接受任何参数并且只能使用无参数形式进行调用的闭包,你必须使用一个显式的空参数列表来声明:
def magicNumber = { -> 42 }
// 下面这个调用将会失败,因为该闭包不接受参数
magicNumber(11)
变长参数
闭包中也是可以像其他正常方法一样使用变长参数列表的。如果闭包的最后一个参数是变长参数或数组的话,它就是一个接受变长参数的闭包,就像下面这些例子中展示的:
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: 接受不定个数的 String 参数的闭包
//2: 可以使用任意数量的字符串参数来直接调用,而不需要事先将这些参数放到一个数组中
//3: 如果参数类型是数组的话,也能达到同样的效果
//4: 只要最后一个参数是数组或显式的变长参数类型,闭包就能接受变长参数
1.3 闭包的委派策略
Groovy 闭包和 lambada 表达式
Groovy 把闭包定义成 Closure 类的实例。这使得它和 Java 8 的 lambada 表达式很不同。委派(delegate)在 Groovy 闭包中是个非常重要的概念,但 lambada 表达式中却并没有委派的概念。改变委派或委派策略的能力,使得使用 Groovy 设计出很棒的领域特定语言(DSL)成为可能。
闭包的 owner, delegate 和 this
为了明白委派的概念,我们必须先解释一下闭包中 this 变量的意义。闭包实际定义了三种不同的实体:
- this 表示包含闭包定义的最近的类的实例
- owner 表示包含闭包定义的最近的类或闭包的实例
- delegate 是一个第三方对象,当闭包内的方法调用或属性访问没有指定接收者时,就会到委派对象上进行方法或属性的解析
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: 相比使用 getThisObject() 方法,一般你可能更喜欢 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'
//1: 闭包在 this 对象上调用了 toString() 方法,这实际上会调用包含闭包定义的最近的类的实例上的 toString() 方法,在这里其实就是 Person 类的实例 p
闭包的 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: 一个定义在 Enclosing 类中的闭包,返回 getOwner() 方法获取到的对象
//2: 调用这个闭包将会返回包含该闭包定义的 Enclosing 类的实例
//3: 通常相比于使用 getOwner() 方法,你可能更喜欢直接使用 owner 这种写法
//4: 这两种方式其实返回相同的对象
//5: 现在闭包被定义在内部类中
//6: 闭包的 owner 将会返回内部类的实例,而不是顶层类的实例
//7: 但是对于嵌套闭包的情况,就像此处 cl 被定义在 nestedClosures 闭包内
//8: owner 对象将会是包含该闭包定义的最近的闭包的实例,即 nestedClosures,而不在是 this
闭包的委派(delegate)
可以使用 delegate 属性或通过 getDelegate() 方法来获取闭包的委派对象。委派是 Groovy 构造领域特定语言的关键概念。和 this 及 owner 这两个闭包词域内的概念不同,delegate 是一个将在闭包中使用的用户自定义的对象。delegate 对象的默认值是闭包的 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: 默认情况下,是 owner,即包含该闭包定义的最近的闭包或类的实例,此处 owner 和 this 是同一个对象
//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')
然后,我们定义一个获取 delegate 对象上 name 属性的闭包:
def upperCasedName = { delegate.name.toUpperCase() }
我们改变闭包的 delegate 对象,就可以看到闭包调用的结果发生变化:
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 却可以透明的使用,也就是说,在方法调用前不需要加前缀:delegate. 下面一节将作介绍
委派策略
任何时刻,闭包内的属性访问如果没有显式的指定属性的接收者时,都会涉及到闭包的委派策略:
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 类的实例 p
//3: 方法调用将会成功
上面这段代码能够工作的原因是:name 属性将会隐式的在 delegate 对象上进行解析。这是一种在闭包内进行属性和方法调用解析的非常强大的方式。这里没有必要显式的指定 delegate. 作为方法或属性的接收者:因为闭包的默认委派策略使其所然。闭包上实际有多种属性解析策略可供选择:
- Closure.OWNER_FIRST 是默认的委派策略。如果闭包的 owner 上存在该属性或方法,那么它们就会在闭包的 owner 对象上进行解析,否则,就会到 delegate 对象上进行该属性或方法的解析。
- Closure.DELEGATE_FIRST 把上面的逻辑反转了:它会现在 delegate 对象上进行方法和属性的解析,delegate 上没定义,才会在 owner 上进行解析
- Closure.OWNER_ONLY 只会在闭包的 owner 对象上进行方法和属性的解析,忽略 delegate
- Closure.DELEGATE_ONLY 只会在闭包的 delegate 对象上进行方法和属性的解析,忽略 owner
- Closure.TO_SELF 可以被那些需要使用高级元编程技术的开发者或像实现定制化的解析策略的开发者使用:此时解析不会在 owner 或 delegate 上进行,只会在闭包类自身上进行。所以仅当你想实现自己的 Closure 子类,才会用到该解析策略。
我们通过下面的代码来展示默认的 OWNER_FIRST 策略:
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: Person 和 Thing 这两个类都定义类 name 属性
//3: 使用默认的解析策略,name 属性将会先在闭包的 owner 实例上查找,此处即为 Person 类的对象 p
//4: 这里我们把委派设置为 Thing 类的实例 t
//5: 方法调用的结果不变,因为 name 会首先在闭包的 owner 上查找
然而,我们可以改变闭包的解析策略:
p.pretty.resolveStrategy = Closure.DELEGATE_FIRST
assert p.toString() == 'My name is Teapot'
通过改变闭包的 resolveStrategy 属性,我们可以改变 Groovy 解析属性的方式:此时,name 属性将会先在 delegate 对象上解析,如果找不到该属性,才会在 owner 对象上解析。因为该例中闭包的 delegate 是 Thing 类的实例 t,它里面含有名为 name 的属性,所以这个属性会被使用。
委派优先(DELEGATE_FIRST)和仅委派(DELEGATE_ONLY)策略或 属主优先(OWNER_FIRST)和仅属主(OWNER_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.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 类有 age 属性。Person 类还声明了一个引用 age 属性的闭包。我们把默认的解析策略从属主优先(OWNER_FIRST)变长仅委派(DELEGATE_ONLY)。闭包的属主是 Person 类的实例 p,当闭包的委派也是 p 时,闭包调用成功,因为 p 对象包含属性 age,但当闭包的委派变长 Thing 类的实例 t 时,闭包调用将会失败,抛出 groovy.lang.MissingPropertyException。我们看到,尽管闭包定义在 Person 类中,闭包的 owner 也没有使用。
1.4 GString 中的闭包
请看下面这段代码:
def x = 1
def gs = "x = ${x}"
assert gs == 'x = 1'
代码的结果正如你预期那样工作,但是如果你改变 x 的值会怎样呢:
x = 2
assert gs == 'x = 2'
如你所见,assert 将会失败,这主要有两个原因:
- GString 只会对值的 toString 表示进行惰性求值
- GString 中的语法 ${x} 并不表示一个闭包,它只是像 $x 的一种表达式,它会在 GString 创建的时候进行求值
在我们的例子中,GString 是使用一个引用了 x 的表达式创建的。当 GString 创建时,x 的值是 1,所以 GString 是以 1 作为值来创建的。当 assert 语句被执行时,GString 会被求值,1 通过 toString 方法被转换成了 String。当我们把 x 设置成 2,我们的确改变的 x 的值,但是它已经是另一个不同的对象了,而 GString 引用的还是那个老的对象。
注意:仅当 GString 引用的值是可变的情况下,GString 才会改变它的 toString 字符串表示。如果只是引用改变,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: 创建一个名为 Sam 的 Person
//3: 创建一个名为 Lucy 的 Person
//4: 变量 p 被设置为 Sam
//5: 创建 引用变量 p 的 GString
//6: 字符串求值时,${p} 返回 Sam
//7: 把 p 设置为 Lucy
//8: GString 求值后仍然得到 Sam,也就是 GString 创建时 p 的值
//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'
1.5 函数式编程
就像 lambada 表达式之于 Java 8 一样,闭包是 Groovy 函数式编程的核心。Closure 类中直接提供了一些函数式编程的操作,如本节将要展示的一样。
科里化 Currying
在 Groovy 里面,科里化表示函数参数部分固定的概念。Groovy 里,科里化能允许你设置闭包中某个参数的值,然后返回一个参数个数减少 1 的新闭包。
左科里化 Left Currying
如下所示,左科里化表示设置闭包参数列表中最左侧参数的值,然后返回一个新的闭包:
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: 调用这个新闭包时,只需传递一个参数
//4: 它和使用 2 作为第一个参数调用 nCopies 闭包等效
右科里化 Right Currying
和左科里化相似,我们也可以设置闭包参数列表中最右侧的参数,这就是右科里化:
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: 这和使用 bla 作为第二个参数调用 nCopies 闭包是等效的
基于索引的科里化
如果一个闭包接受 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 方法将第二个参数(下标为1)的值设置为 2d,返回一个计算体积的新闭包,它接受两个参数:长和高
//3: 这个闭包和使用 2d 作为宽度调用 volume 闭包是等效的
//4: 同样,ncurry 也支持从某个下标起,设置多个参数的值,并返回一个新闭包
//5: 新生成的闭包的参数个数等于原来闭包的参数个数减去使用 ncurry 设置了值的参数个数
记忆化 Memoization
记忆化允许我们缓存闭包调用的结果。如果闭包执行的操作很慢,但是你知道这个闭包将会多次使用相同的参数进行调用,这时记忆化就很有用。关于记忆化的一个经典的例子是计算斐波纳西数列。斐波纳西数列的一个简单的实现如下:
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 个值,最多缓存 m 个值
所有记忆化方法中使用的缓存都是基于 LRU 策略的缓存。
闭包合成 Composition
闭包合成是一个类似于函数合成的概念,也就是说使用两个或多个已有的函数来构造一个新的函数,如下所示:
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)
串行调用 Trampoline
递归程序经常受到最大堆栈大小这个物理限制。如果递归调用太深,最终将会引发栈溢出异常 StackOverflowException。改变这种情况的一个方法是使用闭包 Closure 和它的串行调用(trampoline)能力。闭包将会被包装成 TrampolineClosure,当调用发生时串行化的闭包将会调用原始的闭包并等待它的结果。如果调用结果是另一个 TrampolineClosure 类的实例(这可能是由于调用 trampoline() 方法产生的),那么该原始的闭包会再次被调用。这种对返回的 Trampoline 化的闭包的重复调用会一直进行,直到返回值不再是一个 Trampoline 化的闭包为止。这个返回值就会是串行调用的最终的值。通过这种方式,所有的调用都是顺序进行的,而不是不断的填充堆栈。
下面是一个使用 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
方法指针
如果能将一个常规的方法当作闭包来使用将是非常实用的。例如你可能想使用闭包的科里化能力,但是普通方法却没有这种能力。在 Groovy 中你可以使用方法指针从任何方法来获取一个闭包。
使用方法指针操作符 .& 能够将方法的引用保存到一个变量中,以备后续使用:
def str = 'example of method reference' //1
def fun = str.&toUpperCase //2
def upper = fun() //3
assert upper == str.toUpperCase() //4
//1: 定义一个 String 类型的变量 str
//2: 我们把 str 实例的 toUpperCase 方法存储到 fun 变量中
//3: 我们可以像正常方法一样调用 fun
//4: 得到的结果和直接在 str 上调用 toUpperCase 方法是一样的
使用方法指针有诸多有点:方法指针的类型是 groovy.lang.Closure,因此能用闭包的地方都能使用方法指针。特别是,它非常适合将一个已有的方法转换成策略模式中需要的形式:
def transform(List elements, Closure action) { //1
def result = []
elements.each {
result << action(it)
}
result
}
String describe(Person p) {
"$p.name is $p.age" //2
}
def action = this.&describe //3
def list = [
new Person(name: 'Bob', age: 42),
new Person(name: 'Julia', age: 35)] //4
assert transform(list, action) == ['Bob is 42', 'Julia is 35'] //5
//1: transform 方法会在 elements 中的每一个元素上调用 action 闭包,并切返回一个新的结果列表
//2: 我们定义了一个接受 Person 对象为参数,且返回 String 的方法
//3: 我们创建一个指向 describe 方法的方法指针
//4: 我们创建了一个由 Person 组成的参数列表
//5: 在需要闭包的地方,我们可以传入一个方法指针
方法指针是绑定在方法的接收者和方法名称上的。方法的参数是在运行时解析的。这意味着如果你有多个相同名称的方法,方法指针使用起来也没什么两样,只是解析最终调用的方法是在运行时进行的:
def doSomething(String str) { str.toUpperCase() } //1
def doSomething(Integer x) { 2*x } //2
def reference = this.&doSomething //3
assert reference('foo') == 'FOO' //4
assert reference(123) == 246 //5
//1: 定义一个重载方法 doSomething,它接受一个 String 参数
//2: 定义一个重载方法 doSomething,它接受一个 Integer 参数
//3: 创建一个指向 doSomething 方法的方法指针,这里并未指定参数类型
//4: 以 String 为参数调用方法指针将会使用 doSomething 的 String 版本
//4: 以 Integer 为参数调用方法指针将会使用 doSomething 的 Integer 版本
最后
以上就是合适盼望为你收集整理的Groovy 闭包一站式手册1. 闭包的全部内容,希望文章能够帮你解决Groovy 闭包一站式手册1. 闭包所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复