目录
3.2. Owner, delegate(委托) and this
Groovy中的闭包是一个开放的,匿名的代码块,可以接受参数,可以返回值并且可以赋值给闭包变量。闭包可以引用在其周围范围内声明的变量,与闭包的正式定义相反,Groovy语言中的Closure也可以包含在其周围范围之外定义的自由变量。
1.语法
1.1闭包定义
闭包定义遵循以下语法:
{ [closureParameters -> ] statements }
其中[closureParameters->]是一个以逗号分隔的可选参数列表,而且statements 有0条或更多条Groovy语句,参数看起来类似于方法参数列表,这些参数可以是类型化的或非类型化的。指定参数列表时, - >字符是必需的,用于将参数列表与Groovy语句分开,Groovy语句部分由0,1或许多条Groovy语句组成。
有效的闭包定义的一些示例:
{ item++ } //一个引用名为item的变量的闭包
{ -> item++ } //通过添加箭头( - >)可以明确地将闭包参数与代码分开
{ println it } //使用隐式参数(it)的闭包
{ it -> println it } //上面的一个替代版本,它是一个显式参数
{ name -> println name } //在这种情况下,通常最好为参数使用显式名称
{ String x, int y -> //一个闭包接受两个类型参数
println "hey ${x} the value is ${y}"
}
{ reader -> //闭包可以包含多个语句
def line = reader.readLine()
line.trim()
}
1.2. 闭包作为对象
一个闭包是groovy.lang.Closure类的一个实例,它可以像任何其他变量一样赋值给变量或字段,尽管它是一个代码块:
def code = { 123 }
然后闭包内的代码只会在你调用闭包时执行,这可以通过使用闭包类型的变量来完成,就好像在调用一个常规方法:
assert code() == 123
或者,您可以显式地调用方法:
def isOdd = { int i -> i%2 != 0 } //定义一个接受int作为参数
assert isOdd(3) == true //它可以直接调用
assert isOdd.call(2) == false //或使用闭包的call方法
def isEven = { it%2 == 0 } //具有隐式参数的闭包it
assert isEven(3) == false //直接调用
assert isEven.call(2) == true //使用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 }
// 这个调用会失败,因为该闭包不接受任何参数
magicNumber(11)
2.3. 可变参数
闭包可以像任何其他方法一样声明可变参数。可变参数方法的特点是:参数的数量是可变的。有2种情况:最后一个参数是可变长度的参数,或者是一个数组参数。看下面例子:
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'
3.委托策略
3.1 Groovy闭包 VS lambda表达式
Groovy将闭包定义为Closure类的实例,与Java 8中的lambda表达式截然不同。委托是Groovy闭包中的一个关键概念,它在lambda中没有等价概念。能够更改委托或更改闭包的委托策略使得在Groovy中设计漂亮的域特定语言(DSL)成为可能。
3.2. Owner, delegate(委托) and this
要理解委托(delegate)的概念,首先必须在闭包中解释this的含义。闭包实际上定义了3个不同的东西:
- this对应于定义闭包的那个封闭类实例对象
- owner对应于定义闭包的那个封闭对象,可以是类实例对象或闭包对象
- delegate对应于第三方对象,
3.2.1. this的含义
在闭包中,调用getThisObject将返回定义闭包的那个封闭类。它相当于显式地使用this:
class Enclosing {
void run() {
def whatIsThisObject = { getThisObject() }//闭包是在Enclosing类中定义的,并返回getThisObject
assert whatIsThisObject() == this //调用闭包将返回定义闭包的Enclosing实例
def whatIsThis = { this } //一般来说,您只想使用此符号的快捷方式
assert whatIsThis() == this //它返回完全相同的对象
}
}
class EnclosedInInnerClass {
class Inner {
Closure cl = { this } //如果闭包是在内部类中定义的
}
void run() {
def inner = new Inner()
assert inner.cl() == inner//this在闭包中将返回内部类,而不是顶级类
}
}
class NestedClosures {
void run() {
def nestedClosures = {
def cl = { this }//在嵌套闭包的情况下,像这里的cl在nestedClosures的范围内定义
cl()
}
assert nestedClosures() == this//则有this对应于最接近的外层对象(NestedClosures实例),而不是封闭的nestedClosures闭包!
}
}
当然可以通过上面那种方式(方法内定义闭包)从封闭类中调用方法:
class Person {
String name
int age
String toString() { "$name is $age years old" }
String dump() {
def cl = {
String msg = this.toString()//闭包调用toString,它实际上会调用封闭对象上的toString方法,也就是说Person实例
println msg
msg
}
cl()
}
}
def p = new Person(name:'Janice', age:74)
assert p.dump() == 'Janice is 74 years old'
3.2.2. 闭包的Owner
闭包的Owner与闭包中this的定义非常类似,但有一个微妙的区别:它将直接返回封闭对象,无论返回的是闭包对象还是类实例对象。
class Enclosing {
void run() {
def whatIsOwnerMethod = { getOwner() }//闭包是在Enclosing类中定义的,并返回getOwner
assert whatIsOwnerMethod() == this //调用闭包将返回定义闭包的Enclosing实例
def whatIsOwner = { owner } //通常,您只想使用owner表示法这种快捷方式
assert whatIsOwner() == this //它返回完全相同的对象
}
}
class EnclosedInInnerClass {
class Inner {
Closure cl = { owner } //如果闭包是在内部类中定义的
}
void run() {
def inner = new Inner()
assert inner.cl() == inner //闭包中的owner将返回内部类,而不是顶级类
}
}
class NestedClosures {
void run() {
def nestedClosures = {
def cl = { owner }//但是在嵌套闭包的情况下,像这里cl被定义在nestedClosures的范围内
cl()
}
assert nestedClosures() == nestedClosures//则有owner对应封闭的闭包(nestedClosures),因此这点与this不同!
}
}
3.2.3. 闭包的委托
可以使用delegate属性或调用getDelegate方法来访问闭包的委托。它是在Groovy中构建特定邻域语言的强大概念。默认情况下,闭包的委托被设置为闭包的Owner:
class Enclosing {
void run() {
def cl = { getDelegate() }//你可以通过调用闭包的getDelegate方法获得闭包的委托
def cl2 = { delegate } //或直接使用delegate属性
assert cl() == cl2() //两者都返回相同的对象
assert cl() == this //这是封闭的类或闭包
def enclosed = {
{ -> delegate }.call() //特别是在嵌套闭包的情况下
}
assert enclosed() == enclosed //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'
此时,和下面的效果没有什么不同:
def target = p
def upperCasedNameUsingVar = { target.name.toUpperCase() }
assert upperCasedNameUsingVar() == 'NORMAN'
但是,存在重大差异:
- 在最后一个示例中,target是从闭包内引用的局部变量
- delegate可以透明地使用(它是闭包的一个属性),也就是说没有使用前缀delegate.这样的方法调用。
3.2.4. 委托策略
每当在闭包中访问属性时,若没有显式设置一个属性,就会涉及到委托策略:
class Person {
String name
}
def p = new Person(name:'Igor')
def cl = { name.toUpperCase() } //name不引用在闭包的词法范围中的变量
cl.delegate = p //我们可以将闭包的委托更改为Person的实例
assert cl() == 'IGOR' //方法调用将成功
此代码工作的原因是name属性将在委托对象上被透明地解析!这是解决闭包内属性或方法调用的一种非常强大的方法。闭包实际上定义了多种解析策略,您可以选择:
- Closure.OWNER_FIRST是默认策略。如果闭包的owner上存在该属性/方法,则将在闭包的owner上使用该属性/方法。如果没有,则使用委托上的。
- Closure.DELEGATE_FIRST反转了逻辑:首先使用委托的上的属性/方法,然后再使用owner的
- Closure.DELEGATE_ONLY将仅解析委托上的属性/方法,将忽略owner
- Closure.TO_SELF可供需要高级元编程技术并希望实现自定义解析策略的开发人员使用:解析不会发生在owner或委托上,而只会发生在封闭类本身上。使用它唯一有意义的就是在实现自己的Closure子类的时候。
让我们用这段代码说明默认的“OWNER_FIRST”策略:
class Person {
String name
def pretty = { "My name is $name" }//为了说明,我们定义了一个引用name的闭包成员
String toString() {
pretty()
}
}
class Thing {
String name
}
def p = new Person(name: 'Sarah')
def t = new Thing(name: 'Teapot')//Person和Thing类都定义了一个name属性
assert p.toString() == 'My name is Sarah'//使用默认策略,首先在owner上解析name属性, p.pretty的owner是p
p.pretty.delegate = t //所以,如果我们将委托更改为Thing的一个实例t
assert p.toString() == 'My name is Sarah' //结果没有变化:名称首先在闭包的owner上解析。由于默认策略,p.pretty的owner还是p
但是,可以更改闭包的解决策略:
p.pretty.resolveStrategy = Closure.DELEGATE_FIRST
assert p.toString() == 'My name is Teapot'
在这种情况下,name将首先在委托中查找,如果没有找到,则在owner上查找。由于委托即Thing实例中定义有name属性,因此使用它。 如果委托(或者owner)和他们的封闭类其中之一没有这样的方法或属性会怎么样?看看下面:
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”。由于闭包的owner是Person类实例,因此我们可以检查如果委托是Person实例,则调用闭包是成功的,但是如果我们使用委托作为Thing的实例来调用它,则它会失败并且会抛出groovy .lang.MissingPropertyException异常。有关如何使用此功能开发DSL的全面的说明,请参阅本手册的专用部分。
4. 闭包中的GStrings
def x = 1
def gs = "x = ${x}"
assert gs == 'x = 1'
代码如你所愿,但是如果添加以下内容会发生什么:
x = 2
assert gs == 'x = 2'
你会看到断言失败了!有两个原因:
- GString只会延迟地求引用的值的toString表示。例子中x是引用,1是integer包装类型的值
- GString中的语法$ {x}不是闭包,而是表达式$ x,它在创建GString时求值。
在我们的示例中,GString是使用引用x的表达式创建的,创建GString时,x的值为1,因此创建的GString值为“x = 1”("x="+new Integer(1).toString())。触发断言时,GString开始被创建,1(Integer类型)也被toString转换为String类型,当我们将x更改为2时,我们更改了x的值,但2已经是一个不同的对象了,GString仍然引用旧的1。
一个GString如果它引用的值变异了,那么只会改变它引用的值的toString表示。如果它使用的引用改变了,什么都不会发生。
如果你现在需要一个GSTring中名副其实的闭包,并且还强制延迟对变量(引用变量)求值,您需要使用下面固定示例中的替代语法$ {→x}:
def x = 1
def gs = "x = ${-> x}"
assert gs == 'x = 1'
x = 2
assert gs == 'x = 2'
让我们用这段下面这段代码来说明替代语法$ {→x}与变异的区别:
class Person {
String name
String toString() { name } //Person类有一个返回name属性的toString方法
}
def sam = new Person(name:'Sam') //我们创建了第一个名叫Sam的人
def lucy = new Person(name:'Lucy') //我们创建另一个名叫Lucy的人
def p = sam //p变量设置为Sam
def gs = "Name: ${p}" //并创建一个“伪”闭包,引用p的值,也就是说Sam
assert gs == 'Name: Sam' //所以当我们求得字符串时,它会返回Sam
p = lucy //如果我们改变p为lucy
assert gs == 'Name: Sam' //字符串仍然求值为Sam,因为它是创建GString时的p变量的值
sam.name = 'Lucy' //所以,如果我们改变Sam(p变量的值)将他的名字改为Lucy
assert gs == 'Name: Lucy' //这次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 闭包的强制转型
可以将闭包转换为接口或单抽象方法(single-abstract method)类型,具体参考这里。
6 函数式编程
闭包,类似Java 8中的lambda表达式,是Groovy中函数式编程范例的核心,关于函数的一些函数式编程的操作可直接在Closure类上获得,如本节所示。
6.1. 柯里化
在Groovy中,柯里化的概念只得到了部分应用。它不符合函数式编程中柯里化的真正概念,因为Groovy在闭包上应用了不同的作用域规则。Groovy中的柯里化将允许您设置闭包中一个参数的值,并且它将返回一个接受剩余参数的新闭包。
6.1.1. 左柯里化
左柯里化是设置闭包最左边的参数,就像下面这个例子:
def nCopies = { int n, String str -> str*n } //nCopies闭包定义了两个参数
def twice = nCopies.curry(2) //curry会将第一个参数设置为2,创建一个接受单个String的新闭包(函数)
assert twice('bla') == 'blabla' //所以只用一个String参数调起新闭包(函数)
assert twice('bla') == nCopies(2, 'bla') //它相当于用两个参数调起nCopies
6.1.2. 右柯里化
类似于左柯里化,可以设置闭包的最右边参数:
def nCopies = { int n, String str -> str*n } //nCopies闭包定义了两个参数
def blah = nCopies.rcurry('bla')//rcurry会将最后一个参数设置为bla,创建一个接受单个int的新闭包(函数)
assert blah(2) == 'blabla' //所以只用int类型参数调起新闭包(函数)
assert blah(2) == nCopies(2, 'bla') //相当于用两个参数调起nCopies闭包
6.1.3. 基于索引的柯里化
如果闭包接受超过2个参数,则可以使用ncurry设置任意参数:
def volume = { double l, double w, double h -> l*w*h } //volume函数(闭包可看做函数,只不过闭包把参数列表放到了函数体内部,并可以赋值给一个闭包引用)定义了3个参数
def fixedWidthVolume = volume.ncurry(1, 2d)//ncurry会将第二个参数(index = 1)设置为2d,创建一个接受长度和高度的新体积函数
assert volume(3d, 2d, 4d) == fixedWidthVolume(3d, 4d) //该函数相当于调用省略宽度的volume
def fixedWidthAndHeight = volume.ncurry(1, 2d, 4d) //也可以从指定的索引开始起设置多个参数
assert volume(3d, 2d, 4d) == fixedWidthAndHeight(3d) //...
6.2. 记忆化
记忆化允许缓存闭包调用的结果,有趣的是,函数(闭包)执行的速度很慢,但是你知道这个函数经常会用相同的参数调用。一个典型的例子是Fibonacci,一个幼稚的实现如下所示:
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将生成一个新的闭包,它至少缓存m个值和最多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的蹦床功能。
略,由于时间紧没太看明白,网上有人说这是尾递归。
6.5. 方法指针
使用常规方法作为闭包通常是实用的。例如,您可能希望使用闭包的柯里化功能,但它不适用于普通方法。在Groovy中,您可以使用方法指针运算符从任何方法上获取闭包。
6.6 闭包作为方法参数的简略写法
当闭包作为闭包或方法的最后一个参数,可以将闭包从参数圆括号中提取出来接在最后,如果闭包是唯一的一个参数,则闭包或方法参数所在的圆括号也可以省略。对于有多个闭包参数的,只要是在参数声明最后的,均可以按上述方式省略。
def fun1(arg1,ard2,argN){
...
}
//fun1方法调用的写法:
fun1 arg1,ard2,argN
def fun(args...,Closure c){
...
}
//fun方法调用写法一:
fun args...,{}
//fun方法调用写法二:
fun(args...){}
原文出处:http://groovy-lang.org/closures.html
翻译不易,且行且珍惜,转载请注明出处。
你的打赏是我奋笔疾书的动力!
支付宝打赏:
微信打赏: