5. @DelegatesTo
5.1. 编译时解释委托策略
@groovy.lang.DelegatesTo
是一个文档与编译时注释,它的主要作用在于:
- 记录使用闭包做为参数的 API。
- 为静态类型检查器与编译器提供类型信息。
Groovy 是构建 DSL 的一种选择平台。使用闭包可以非常轻松地创建自定义控制结构,创建构建者也非常方便。比如有下面这样的代码:
email {
from 'dsl-guru@mycompany.com'
to 'john.doe@waitaminute.com'
subject 'The pope has resigned!'
body {
p 'Really, the pope has resigned!'
}
}
使用构建者策略可实现,利用一个参数为闭包的名为 email
的方法,它会将随后的调用委托给一个对象,该对象实现了 from
、 to
、 subject
及 body
各方法。 body
方法使用闭包做参数,使用的是构建者策略。
实现这样的构建者往往要通过下面的方式:
def email(Closure cl) {
def email = new EmailSpec()
def code = cl.rehydrate(email, this, this)
code.resolveStrategy = Closure.DELEGATE_ONLY
code()
}
EmailSpec
类实现了 from
、 to
等方法,通过调用 rehydrate
,创建了一个闭包副本,用于为该副本设置 delegate
、 owner
及 thisObject
等值。设置 owner
和 thisObject
并不十分重要,因为将使用 DELEGATE_ONLY
策略,解决方法调用只针对的是闭包委托。
class EmailSpec {
void from(String from) { println "From: $from"}
void to(String... to) { println "To: $to"}
void subject(String subject) { println "Subject: $subject"}
void body(Closure body) {
def bodySpec = new BodySpec()
def code = body.rehydrate(bodySpec, this, this)
code.resolveStrategy = Closure.DELEGATE_ONLY
code()
}
}
The EmailSpec
类自身的 body
方法将接受一个复制并执行的闭包,这就是 Groovy 构建者模式的原理。
代码中的一个问题在于, email
方法的用户并不知道他能在闭包内调用的方法。唯一的了解途径大概就是方法文档。不过这样也存在两个问题:首先,有些内容并不一定会写出文档,如果记下文档,人们也不一定总能获得(比如 Javadoc 就是在线的,没有下载版本);其二,也没有帮助 IDE。假如有 IDE 来辅助开发者,比如一旦当他们在闭包体内,就建议采用 email
类中的方法。
如果用户调用了闭包内的一个方法,该方法没有被 EmailSpec
类所定义,IDE 应该至少能提供一个警告(因为这非常有可能会在运行时造成崩溃)。
上面代码还存在的一个问题是,它与静态类型检查不兼容。类型检查会让用户了解方法调用是否在编译时被授权(而不是在运行时),但如果你对下面这种代码执行类型检查的话:
email {
from 'dsl-guru@mycompany.com'
to 'john.doe@waitaminute.com'
subject 'The pope has resigned!'
body {
p 'Really, the pope has resigned!'
}
}
类型检查器当然知道存在一个接收 Closure
的 email
方法,但是它会为闭包 内 的每个方法都进行解释,比如说 from
,它不是一个定义在类中的方法,实际上它定义在 EmailSpec
类中,但在运行时,没有任何线索能让检查器知道它的闭包委托类型是 EmailSpec
:
@groovy.transform.TypeChecked
void sendEmail() {
email {
from 'dsl-guru@mycompany.com'
to 'john.doe@waitaminute.com'
subject 'The pope has resigned!'
body {
p 'Really, the pope has resigned!'
}
}
}
所以在编译时会失败,错误信息如下:
[Static type checking] - Cannot find matching method MyScript#from(java.lang.String). Please check if the declared type is right and if the method exists.
@ line 31, column 21.
from 'dsl-guru@mycompany.com'
5.2. @DelegatesTo
基于以上这些原因,Groovy 2.1 引入了一个新的注释: @DelegatesTo
。该注释目的在于解决文档问题,让 IDE 了解闭包体内的期望方法。同时,它还能够给编译器提供一些提示,告知编译器闭包体内的方法调用的可能接收者是谁,从而解决类型检查问题。
具体方法是注释 email
方法中的 Closure
参数:
def email(@DelegatesTo(EmailSpec) Closure cl) {
def email = new EmailSpec()
def code = cl.rehydrate(email, this, this)
code.resolveStrategy = Closure.DELEGATE_ONLY
code()
}
上面代码告诉编译器(或者 IDE)闭包内的方法何时被调用,闭包委托被设置为 email
类型对象。但这里仍遗漏了一个问题:默认的委托策略并非方法所使用的那一种。因此,我们还需要提供更多信息,告诉编译器(或 IDE)委托策略也改变了。
def email(@DelegatesTo(strategy=Closure.DELEGATE_ONLY, value=EmailSpec) Closure cl) {
def email = new EmailSpec()
def code = cl.rehydrate(email, this, this)
code.resolveStrategy = Closure.DELEGATE_ONLY
code()
}
现在,IDE 和类型检查器(如果使用 @TypeChecked
)都能知道委托和委托策略了。现在,IDE 不仅可以进行智能补足,而且还能消除编译时出现的错误,而这种错误的产生,通常只是因为程序行为只有到了运行时才被知晓。
下面的代码编译起来没有任何问题了:
@TypeChecked
void doEmail() {
email {
from 'dsl-guru@mycompany.com'
to 'john.doe@waitaminute.com'
subject 'The pope has resigned!'
body {
p 'Really, the pope has resigned!'
}
}
}
5.3. DelegatesTo 模式
@DelegatesTo
支持多种模式,本部分内容将予以详细介绍。
5.3.1. 简单委托
该模式中唯一强制的参数是 value,它指明了委托调用的类。除此之外没有别的。编译器还将知道:委托类型将一直是由 @DelegatesTo
所记录的类型。(注意它可能是个子类,如果是子类的话,对于类型检查器来说,由该子类所定义的方法是可见的。)
void body(@DelegatesTo(BodySpec) Closure cl) {
// ...
}
5.3.2. 委托策略
在该模式中,必须指定委托类和委托策略。如果闭包没有以缺省的委托策略( Closure.OWNER_FIRST
)进行调用,就必须使用该模式。
void body(@DelegatesTo(strategy=Closure.DELEGATE_ONLY, value=BodySpec) Closure cl) {
// ...
}
5.3.3. 委托给参数
在这种形式中,我们将会告诉编译器将委托给方法的另一个参数。如下所示:
def exec(Object target, Closure code) {
def clone = code.rehydrate(target, this, this)
clone()
}
这里所用的委托不是在 exec
方法内创建的。实际上是拿了方法中的一个参数然后委托给它。如下所示:
def email = new Email()
exec(email) {
from '...'
to '...'
send()
}
每个方法调用都委托给了 email
参数。这是一种应用很广的模式,它也能被使用联合注释 @DelegatesTo
所支持。
def exec(@DelegatesTo.Target Object target, @DelegatesTo Closure code) {
def clone = code.rehydrate(target, this, this)
clone()
}
闭包使用了注释 @DelegatesTo
,但这一次,没有指定任何类,而利用 @DelegatesTo.Target
注释了另一个参数。委托类型在编译时进行指定。可能有人会认为使用参数类型,比如在该例中是 Object
,但这是错的。代码如下:
class Greeter {
void sayHello() { println 'Hello' }
}
def greeter = new Greeter()
exec(greeter) {
sayHello()
}
注意,这不需要利用 @DelegatesTo
注释。但是,要想让 IDE 或者 类型检查器 知道委托类型,我们需要 @DelegatesTo
。本例中, Greeter
变量属于 Greeter
类型, 而且即使 exec
方法并没有明显地定义 Greeter
类型的目标 , sayHello
方法也不会报出错误。这种功能非常有用,可以避免我们针对不同的接收类型而编写不同的 exec
方法。
该模式下, @DelegatesTo
注释也支持我们上面介绍的 strategy
参数。
5.3.4 多个闭包
前例中, exec
方法只接受一个闭包,但是可能会有接收多个闭包的方法:
void fooBarBaz(Closure foo, Closure bar, Closure baz) {
...
}
利用 @DelegatesTo
注释每个闭包就显得不可避免了:
class Foo { void foo(String msg) { println "Foo ${msg}!" } }
class Bar { void bar(int x) { println "Bar ${x}!" } }
class Baz { void baz(Date d) { println "Baz ${d}!" } }
void fooBarBaz(@DelegatesTo(Foo) Closure foo, @DelegatesTo(Bar) Closure bar, @DelegatesTo(Baz) Closure baz) {
...
}
但更重要的是,如果有多个闭包和多个参数,可以使用一些目标:
void fooBarBaz(
@DelegatesTo.Target('foo') foo,
@DelegatesTo.Target('bar') bar,
@DelegatesTo.Target('baz') baz,
@DelegatesTo(target='foo') Closure cl1,
@DelegatesTo(target='bar') Closure cl2,
@DelegatesTo(target='baz') Closure cl3) {
cl1.rehydrate(foo, this, this).call()
cl2.rehydrate(bar, this, this).call()
cl3.rehydrate(baz, this, this).call()
}
def a = new Foo()
def b = new Bar()
def c = new Baz()
fooBarBaz(
a, b, c,
{ foo('Hello') },
{ bar(123) },
{ baz(new Date()) }
)
这时,你可能会感到困惑:为何我们不把引用作为参数名呢?因为相关信息(参数名)不一定能获取到(只供调试的信息),所以这是 JVM 的一个缺陷。
5.3.5. 委托给基本类型
在一些情况下,可以命令 IDE 或编译器,使委托类型不是参数而是某种基本类型。假设有下面这样运行在一列元素上的配置器:
public <T> void configure(List<T> elements, Closure configuration) {
elements.each { e->
def clone = configuration.rehydrate(e, this, this)
clone.resolveStrategy = Closure.DELEGATE_FIRST
clone.call()
}
}
然后利用任何列表都可以调用该方法:
@groovy.transform.ToString
class Realm {
String name
}
List<Realm> list = []
3.times { list << new Realm() }
configure(list) {
name = 'My Realm'
}
assert list.every { it.name == 'My Realm' }
要想让类型检查器和 IDE 了解 configure
方法在列表的每个元素上调用闭包,你需要换一种方式来使用 @DelegatesTo
:
public <T> void configure(
@DelegatesTo.Target List<T> elements,
@DelegatesTo(strategy=Closure.DELEGATE_FIRST, genericTypeIndex=0) Closure configuration) {
def clone = configuration.rehydrate(e, this, this)
clone.resolveStrategy = Closure.DELEGATE_FIRST
clone.call()
}
@DelegatesTo
获取一个可选的 genericTypeIndex
参数,该参数指出被用作委托类型的基本类型的具体索引。它 必须 要与 @DelegatesTo.Target
联合使用,并且起始索引要为 0。在上例中,委托类型会根据 List<T>
来判定,因为在索引 0 处的基本类型是 T
,并推断是 Realm
,所以类型检查器也会推断委托类型属于 Realm
类型。
由于 JVM 的限制,我们使用 genericTypeIndex
来代替占位符( T
)。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论