返回介绍

5. @DelegatesTo

发布于 2025-01-04 00:44:53 字数 8672 浏览 0 评论 0 收藏 0

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 的方法,它会将随后的调用委托给一个对象,该对象实现了 fromtosubjectbody 各方法。 body 方法使用闭包做参数,使用的是构建者策略。

实现这样的构建者往往要通过下面的方式:

def email(Closure cl) {
  def email = new EmailSpec()
  def code = cl.rehydrate(email, this, this)
  code.resolveStrategy = Closure.DELEGATE_ONLY
  code()
}

EmailSpec 类实现了 fromto 等方法,通过调用 rehydrate ,创建了一个闭包副本,用于为该副本设置 delegateownerthisObject 等值。设置 ownerthisObject 并不十分重要,因为将使用 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!'
  }
}

类型检查器当然知道存在一个接收 Closureemail 方法,但是它会为闭包 的每个方法都进行解释,比如说 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文