使用Groovy实现Domain-Specific Languages

本文翻译自官方文档

1. Command chains 链式调用

Groovy lets you omit parentheses around the arguments of a method call for top-level statements. "command chain" feature extends this by allowing us to chain such parentheses-free method calls, requiring neither parentheses around arguments, nor dots between the chained calls. The general idea is that a call like a b c d will actually be equivalent to a(b).c(d). This also works with multiple arguments, closure arguments, and even named arguments. Furthermore, such command chains can also appear on the right-hand side of assignments. Let’s have a look at some examples supported by this new syntax:

Groovy 允许函数调用的时候不写括号,"链式调用"特性又允许函数调用不写点(.),两者配合,使得函数调用的时候既不需要写括号,又不需要写点(.)。比如说a b c d等价于a(b).c(d)。这个特性同样适用多参数、闭包参数、命名参数的情况。此外,这个调用链也可以放在赋值语句的右边。下面是一些例子:

// equivalent to: turn(left).then(right)
turn left then right

// equivalent to: take(2.pills).of(chloroquinine).after(6.hours)
take 2.pills of chloroquinine after 6.hours

// equivalent to: paint(wall).with(red, green).and(yellow)
paint wall with red, green and yellow

// with named parameters too
// equivalent to: check(that: margarita).tastes(good)
check that: margarita tastes good

// with closures as parameters
// equivalent to: given({}).when({}).then({})
given { } when { } then { }

It is also possible to use methods in the chain which take no arguments, but in that case, the parentheses are needed:

在没有参数的时候,括号不能省略:

// equivalent to: select(all).unique().from(names)
select all unique() from names

If your command chain contains an odd number of elements, the chain will be composed of method / arguments, and will finish by a final property access:

如果链式调用包含基数个元素,那么这个链式调用的结尾就是一个获取属性的操作:

// equivalent to: take(3).cookies
// and also this: take(3).getCookies()
take 3 cookies

这里就揭示了一个规律,链式调用正常由偶数个元素组成,奇数位置是函数名,偶数位置是参数a b c d等于a(b).c(d),上面那个例子paint wall with red, green and yellowred, green中间是逗号隔开,所以是一个整体,算一个,另外没有参数的时候,括号不能省略,也是为了遵从这一规律。

This command chain approach opens up interesting possibilities in terms of the much wider range of DSLs which can now be written in Groovy.

链式调用为使用Groovy实现DSLs提供了非常有趣的可能性。

The above examples illustrate using a command chain based DSL but not how to create one. There are various strategies that you can use, but to illustrate creating such a DSL, we will show a couple of examples – first using maps and Closures:

上面的例子只是展示了基于链式调用的于DSL,但是没说怎么实现。接下来将我们使用maps和Closures,来实现这种DSL:

show = { println it }
square_root = { Math.sqrt(it) }

def please(action) {
  [the: { what ->
    [of: { n -> action(what(n)) }]
  }]
}

// equivalent to: please(show).the(square_root).of(100)
please show the square_root of 100
// ==> 10.0

map的语法可以参考这里,简单来说对于一个map:def colors = [red: '#FF0000', green: '#00FF00', blue: '#0000FF'],有两种方法获取key的值colors['red']colors.green

As a second example, consider how you might write a DSL for simplifying one of your existing APIs. Maybe you need to put this code in front of customers, business analysts or testers who might be not hard-core Java developers. We’ll use the Splitter from the Google Guava libraries project as it already has a nice Fluent API. Here is how we might use it out of the box:

第二个例子,我们以Google Guava libraries中的Splitter为例子,对于这种已有函数,代码一般会写成这样:

@Grab('com.google.guava:guava:r09')
import com.google.common.base.*
def result = Splitter.on(',').trimResults(CharMatcher.is('_' as char)).split("_a ,_b_ ,c__").iterator().toList()

It reads fairly well for a Java developer but if that is not your target audience or you have many such statements to write, it could be considered a little verbose. Again, there are many options for writing a DSL. We’ll keep it simple with Maps and Closures. We’ll first write a helper method:

对一个Java程序员来说,这个代码写得挺漂亮,但是有一说一,确实有点冗长。有很多方法可以把他改造成DSL,但是这里我们将以Maps 和 Closures为例来简化他,定义一个帮助函数:

@Grab('com.google.guava:guava:r09')
import com.google.common.base.*
def split(string) {
  [on: { sep ->
    [trimming: { trimChar ->
      Splitter.on(sep).trimResults(CharMatcher.is(trimChar as char)).split(string).iterator().toList()
    }]
  }]
}

now instead of this line from our original example:

有了这个帮助函数,原来这段代码:

def result = Splitter.on(',').trimResults(CharMatcher.is('_' as char)).split("_a ,_b_ ,c__").iterator().toList()

we can write this:

就可以写成这个样子:

def result = split "_a ,_b_ ,c__" on ',' trimming '_\'

2. Operator overloading 操作符重载

Various operators in Groovy are mapped onto regular method calls on objects.

This allows you to provide your own Java or Groovy objects which can take advantage of operator overloading. The following table describes the operators supported in Groovy and the methods they map to.

在Groovy语言中,很多操作符都映射到常规的方法调用,这就可以让你自己定义的 Java或者Groovy类也能使用操作符。

Operator Method
a + b a.plus(b)
a - b a.minus(b)
a * b a.multiply(b)
a ** b a.power(b)
a / b a.div(b)
a % b a.mod(b)
a | b a.or(b)
a & b a.and(b)
a ^ b a.xor(b)
a++ or ++a a.next()
a-- or --a a.previous()
a[b] a.getAt(b)
a[b] = c a.putAt(b, c)
a << b a.leftShift(b)
a >> b a.rightShift(b)
a >>> b a.rightShiftUnsigned(b)
switch(a) { case(b) : } b.isCase(a)
if(a) a.asBoolean()
~a a.bitwiseNegate()
-a a.negative()
+a a.positive()
a as b a.asType(b)
a == b a.equals(b)
a != b ! a.equals(b)
a <=> b a.compareTo(b)
a > b a.compareTo(b) > 0
a >= b a.compareTo(b) >= 0
a \< b a.compareTo(b) < 0
a <= b a.compareTo(b) <= 0

3. Script base classes 脚本基类

3.1 The Script class 脚本类

Groovy scripts are always compiled to classes. For example, a script as simple as:

Groovy脚本通常被编译成类,比如下面这个脚本:

println 'Hello from Groovy'

is compiled to a class extending the abstract groovy.lang.Script class. This class contains a single abstract method called run. When a script is compiled, then its body will become the run method, while the other methods found in the script are found in the implementing class. The Script class provides base support for integration with your application through the Binding object, as illustrated in this example:

当一个脚本被编译的时候,它将被编译成一个继承 groovy.lang.Script (这个类是abstract 的,它有一个虚函数run)的类,脚本的内容就会变成run函数的内容,脚本中别的函数就会变成这个类的成员函数。这个类可以通过Binding对象,在你的应用程序和脚本之间进行交互,比如:

def binding = new Binding()             //1 a binding is used to share data between the script and the calling class
def shell = new GroovyShell(binding)    //2 a GroovyShell can be used with this binding
binding.setVariable('x',1)              //3 input variables are set from the calling class inside the binding
binding.setVariable('y',3)
shell.evaluate 'z=2*x+y'                //4 then the script is evaluated
assert binding.getVariable('z') == 5    //5 and the `z` variable has been "exported" into the binding
  1. binding可以用来共享数据
  2. GroovyShell接受binding参数
  3. 通过binding向脚本传递变量
  4. 执行脚本
  5. 脚本可以通过binding暴露数据

This is a very practical way to share data between the caller and the script, however it may be insufficient or not practical in some cases. For that purpose, Groovy allows you to set your own base script class. A base script class has to extend groovy.lang.Script and be a single abstract method type:

这是在调用者和脚本之间传递数据最基础的做法,但是不够灵活。在Groovy调用脚本的时候,还可以使用自定义的脚本类:

abstract class MyBaseClass extends Script {
    String name
    public void greet() { println "Hello, $name!" }
}

Then the custom script base class can be declared in the compiler configuration, for example:

然后通过CompilerConfiguration对象指定自定义脚本基类:

def config = new CompilerConfiguration()                                //1
config.scriptBaseClass = 'MyBaseClass'                                  //2
def shell = new GroovyShell(this.class.classLoader, config)             //3
shell.evaluate """
    setName 'Judith'                                                    //4
    greet()
"""
  1. 定义一个CompilerConfiguration
  2. 指定脚本类
  3. 创建GroovyShell
  4. 现在脚本可以直接设置name属性,调用greet函数

3.2. The @BaseScript annotation @BaseScript注解

As an alternative, it is also possible to use the @BaseScript annotation directly into a script:

另外,可以直接在脚本里使用@BaseScript注解:

import groovy.transform.BaseScript

@BaseScript MyBaseClass baseScript
setName 'Judith'
greet()

where @BaseScript should annotate a variable which type is the class of the base script. Alternatively, you can set the base script class as a member of the @BaseScript annotation itself:

但是@BaseScript只能标注一个变量,所以还可以写成这样:

@BaseScript(MyBaseClass)
import groovy.transform.BaseScript

setName 'Judith'
greet()

这里需要注意:@BaseScript(MyBaseClass)经过我的测试,一定要放在文件的第一页。

另外通过使用这个注解,IDEA里面是可以支持代码提示的。

3.3. Alternate abstract method 虚函数的替身

We have seen that the base script class is a single abstract method type that needs to implement the run method. The run method is executed by the script engine automatically. In some circumstances it may be interesting to have a base class which implements the run method, but provides an alternative abstract method to be used for the script body. For example, the base script run method might perform some initialization before the run method is executed. This is possible by doing this:

通过上面的例子,我们可以看到,脚本引擎会自动执行脚本类的run函数,但是有时候,我们希望自定义的脚本基类能够通过实现run函数,从而进行一些初始化操作,这其实也是可以的,只要重新定义一个虚函数:

abstract class MyBaseClass extends Script {
    int count
    abstract void scriptBody()                              //1
    def run() {
        count++                                             //2
        scriptBody()                                        //3
        count                                               //4
    }
}
  1. 脚本基类重新定义唯一一个虚函数
  2. run函数在运行脚本内容之前可以做点别的事情
  3. run函数调用scriptBody,从而执行脚本类容
  4. 返回自定义内容,而不是脚本的内容

If you execute this code:

def result = shell.evaluate """
    println 'Ok'
"""
assert result == 1

Then you will see that the script is executed, but the result of the evaluation is 1 as returned by the run method of the base class. It is even clearer if you use parse instead of evaluate, because it would allow you to execute the run method several times on the same script instance:

def script = shell.parse("println 'Ok'")
assert script.run() == 1
assert script.run() == 2

运行第一段代码,返回结果是1,如果使用parse多次运行脚本,返回结果就是1、2、3……

4. Adding properties to numbers 给数字添加属性

In Groovy number types are considered equal to any other types. As such, it is possible to enhance numbers by adding properties or methods to them. This can be very handy when dealing with measurable quantities for example. Details about how existing classes can be enhanced in Groovy are found in the extension modules section or the categories section.

在Groovy语言中,数字类型和其它所有类型地位相同,因此,可以通过为数字添加属性或者函数,来对数字类型进行增强。这个在你处理一个可测量的数据是非常有用。关于增强一个已有类型的内容可以参考这两个地方 extension modules section or the categories section。

An illustration of this can be found in Groovy using the TimeCategory:

下面以 TimeCategory为例:

use(TimeCategory)  {
    println 1.minute.from.now       //1
    println 10.hours.ago

    def someDate = new Date()       //2
    println someDate - 3.months
}
  1. 通过TimeCategory,给Integer添加了一个minute属性。
  2. 类似的,months函数返回一个 groovy.time.DatumDependentDuration类型,支持计算。

Categories are lexically bound, making them a great fit for internal DSLs.

这个给人的感觉就是语言的一部分,用在DSL中就很赞。

5. @DelegatesTo

5.1. Explaining delegation strategy at compile time 什么是编译时委托策略

@groovy.lang.DelegatesTo is a documentation and compile-time annotation aimed at:

  • documenting APIs that use closures as arguments
  • providing type information for the static type checker and compiler

@groovy.lang.DelegatesTo 是一个注解,它的主要用途是:

  • 在将一个闭包作为参数使用时,记录它的API
  • 为静态类型检查以及编译器提供类型信息

The Groovy language is a platform of choice for building DSLs. Using closures, it’s quite easy to create custom control structures, as well as it is simple to create builders. Imagine that you have the following code:

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!'
    }
}

One way of implementing this is using the builder strategy, which implies a method, named email which accepts a closure as an argument. The method may delegate subsequent calls to an object that implements the from, to, subject and body methods. Again, body is a method which accepts a closure as an argument and that uses the builder strategy.

实现这种效果的一种方法是使用构造器策略,定义一个email的函数,它接受一个闭包作为参数。这个函数可以将后续调用委托给实现“from”、“to”、“subject”和“body”方法的对象。同样,body也是一个接受闭包作为参数并使用构建器策略的方法。

Implementing such a builder is usually done the following way:

通常通过一下方式实现构造器:

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

the EmailSpec class implements the from, to, … methods. By calling rehydrate, we’re creating a copy of the closure for which we set the delegate, owner and thisObject values. Setting the owner and the this object is not very important here since we will use the DELEGATE_ONLY strategy which says that the method calls will be resolved only against the delegate of the closure.

首先EmailSpec类实现了 from, to这些函数。接着,通过rehydrate函数,我们创建了原始闭包的一个副本,三个参数分别是: delegate, owner and thisObject,在这个例子中,后面两个参数不重要,只是随便赋个值,因为在第三行我们设置了 DELEGATE_ONLY 策略,它的效果是闭包里面的那些函数统统都在email中找:

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 class has itself a body method accepting a closure that is cloned and executed. This is what we call the builder pattern in Groovy.

EmailSpec 类本身有一个 body 函数,该函数接受闭包。 这就是我们在 Groovy 中所说的构建器模式。

One of the problems with the code that we’ve shown is that the user of the email method doesn’t have any information about the methods that he’s allowed to call inside the closure. The only possible information is from the method documentation. There are two issues with this: first of all, documentation is not always written, and if it is, it’s not always available (javadoc not downloaded, for example). Second, it doesn’t help IDEs. What would be really interesting, here, is for IDEs to help the developer by suggesting, once they are in the closure body, methods that exist on the email class.

但是这有个问题,就是如果写代码的人不看文档,或者没有文档,他就不知道他能用什么功能,另外就是对于IDE来说,他也没法提供代码提示功能。

Moreover, if the user calls a method in the closure which is not defined by the EmailSpec class, the IDE should at least issue a warning (because it’s very likely that it will break at runtime).

此外,如果用户在闭包中调用了 EmailSpec 类未定义的方法,IDE 至少应该发出警告(因为它很可能会在运行时中断)。

One more problem with the code above is that it is not compatible with static type checking. Type checking would let the user know if a method call is authorized at compile time instead of runtime, but if you try to perform type checking on this code:

上面代码的另一个问题是它与静态类型检查不兼容,类型检查可以让问题在编译时而不是运行时就暴露出来,但是如果您尝试对此代码执行类型检查:

email {
    from 'dsl-guru@mycompany.com'
    to 'john.doe@waitaminute.com'
    subject 'The pope has resigned!'
    body {
        p 'Really, the pope has resigned!'
    }
}

Then the type checker will know that there’s an email method accepting a Closure, but it will complain for every method call inside the closure, because from, for example, is not a method which is defined in the class. Indeed, it’s defined in the EmailSpec class and it has absolutely no hint to help it knowing that the closure delegate will, at runtime, be of type EmailSpec:

类型检查工具知道有个email方法,它接受一个Closure作为参数,这个没问题,但是当他检查闭包内部的函数的时候,他就懵逼了。

@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!'
        }
    }
}

will fail compilation with errors like this one:

最终它有可能会报错:

[Static type checking] - Cannot find matching method MyScript#from(java.lang.String). Please check if the declared type is correct and if the method exists.
 @ line 31, column 21.
                       from 'dsl-guru@mycompany.com'

5.2. @DelegatesTo

For those reasons, Groovy 2.1 introduced a new annotation named @DelegatesTo. The goal of this annotation is to solve both the documentation issue, that will let your IDE know about the expected methods in the closure body, and it will also solve the type checking issue, by giving hints to the compiler about what are the potential receivers of method calls in the closure body.

为了解决这个问题,Groovy 2.1引入了一个新注解@DelegatesTo

The idea is to annotate the Closure parameter of the email method:

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()
}

What we’ve done here is telling the compiler (or the IDE) that when the method will be called with a closure, the delegate of this closure will be set to an object of type email. But there is still a problem: the default delegation strategy is not the one which is used in our method. So we will give more information and tell the compiler (or the IDE) that the delegation strategy is also changed:

但是我们在代码中设置了代理策略是DELEGATE_ONLY,所以还需要再注释中添加策略信息:

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()
}

Now, both the IDE and the type checker (if you are using @TypeChecked) will be aware of the delegate and the delegation strategy. This is very nice because it will both allow the IDE to provide smart completion, but it will also remove errors at compile time that exist only because the behaviour of the program is normally only known at runtime!

这就很完美。

The following code will now pass compilation:

@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 modes 代理模式

@DelegatesTo supports multiple modes that we will describe with examples in this section.

下面介绍一下代理模式

5.3.1. Simple delegation 简单代理

In this mode, the only mandatory parameter is the value which says to which class we delegate calls. Nothing more. We’re telling the compiler that the type of the delegate will always be of the type documented by @DelegatesTo (note that it can be a subclass, but if it is, the methods defined by the subclass will not be visible to the type checker).

再这种模式下,我们告诉编译器我们只会再我们注解的类型里面解析(即使是注解类型的子类型,子类型里面定义的内容也是不可以见的)

但是我觉得这个地方说得不对,也可能是我没理解不对。

void body(@DelegatesTo(BodySpec) Closure cl) {
    // ...
}

5.3.2. Delegation strategy 代理策略

In this mode, you must specify both the delegate class and a delegation strategy. This must be used if the closure will not be called with the default delegation strategy, which is Closure.OWNER_FIRST.

在这种模式下,可以额外执行代理策略,默认的策略是:Closure.OWNER_FIRST

void body(@DelegatesTo(strategy=Closure.DELEGATE_ONLY, value=BodySpec) Closure cl) {
    // ...
}

5.3.3. Delegate to parameter 代理变量

In this variant, we will tell the compiler that we are delegating to another parameter of the method. Take the following code:

在这里,我打算直接代理一个变量

上面的例子,都是写死了被代理的对象类型,现在希望我们提供什么类型,就代理什么类型

def exec(Object target, Closure code) {
   def clone = code.rehydrate(target, this, this)
   clone()
}

Here, the delegate which will be used is not created inside the exec method. In fact, we take an argument of the method and delegate to it. Usage may look like this:

在这里,我们要代理的对象不是在exec函数里面创建的,而是通过参数传进来的:

def email = new Email()
exec(email) {
   from '...'
   to '...'
   send()
}

Each of the method calls are delegated to the email parameter. This is a widely used pattern which is also supported by @DelegatesTo using a companion annotation:

这也是@DelegatesTo另外一个很常用的一种用法:

def exec(@DelegatesTo.Target Object target, @DelegatesTo Closure code) {
   def clone = code.rehydrate(target, this, this)
   clone()
}

A closure is annotated with @DelegatesTo, but this time, without specifying any class. Instead, we’re annotating another parameter with @DelegatesTo.Target. The type of the delegate is then determined at compile time. One could think that we are using the parameter type, which in this case is Object but this is not true. Take this code:

closure还是用@DelegatesTo标注,但是另外参数使用 @DelegatesTo.Target注解,这样就行了:

class Greeter {
   void sayHello() { println 'Hello' }
}
def greeter = new Greeter()
exec(greeter) {
   sayHello()
}

Remember that this works out of the box without having to annotate with @DelegatesTo. However, to make the IDE aware of the delegate type, or the type checker aware of it, we need to add @DelegatesTo. And in this case, it will know that the Greeter variable is of type Greeter, so it will not report errors on the sayHello method even if the exec method doesn’t explicitly define the target as of type Greeter. This is a very powerful feature, because it prevents you from writing multiple versions of the same exec method for different receiver types!

其实这里不用 @DelegatesTo标注也是可以的,但是这里加上 @DelegatesTo可以给IDE提供额外的信息,这真是一个非常牛逼特性。

In this mode, the @DelegatesTo annotation also supports the strategy parameter that we’ve described upper.

在这个模式中, @DelegatesTo 注解依然支持 strategy

5.3.4. Multiple closures 多个闭包的情况

In the previous example, the exec method accepted only one closure, but you may have methods that take multiple closures:

上面的例子,只有一个闭包,但是如果有多个闭包的情况:

void fooBarBaz(Closure foo, Closure bar, Closure baz) {
    ...
}

Then nothing prevents you from annotating each closure with @DelegatesTo:

依然可以使用@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) {
   ...
}

But more importantly, if you have multiple closures and multiple arguments, you can use several targets:

还有更牛逼了,多个代理变量、多个闭包。

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()) }
)

At this point, you may wonder why we don’t use the parameter names as references. The reason is that the information (the parameter name) is not always available (it’s a debug-only information), so it’s a limitation of the JVM.

你可能会觉得奇怪,这里为什么不直接通过指定变量名来建立对应关系,这是JVM的一个限制。

5.3.5. Delegating to a generic type 对泛型的代理

In some situations, it is interesting to instruct the IDE or the compiler that the delegate type will not be a parameter but a generic type. Imagine a configurator that runs on a list of elements:

有时候,对于一个泛型类型,我们怎么让编译器或者IDE知道我们代理的是什么类型呢:

注意这里的策略是Closure.DELEGATE_FIRST,我猜是第一个元素类型的意思

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()
   }
}

Then this method can be called with any list like this:

然后这么使用:

@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' }

To let the type checker and the IDE know that the configure method calls the closure on each element of the list, you need to use @DelegatesTo differently:

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 takes an optional genericTypeIndex argument that tells what is the index of the generic type that will be used as the delegate type. This must be used in conjunction with @DelegatesTo.Target and the index starts at 0. In the example above, that means that the delegate type is resolved against List<T>, and since the generic type at index 0 is T and inferred as a Realm, the type checker infers that the delegate type will be of type Realm.

这里主要是genericTypeIndex参数表示第几个泛型类型,这里第0个,就是Realm

We’re using a genericTypeIndex instead of a placeholder (T) because of JVM limitations.

同样是由于JVM的限制,使用genericTypeIndex而不是T

5.3.6. Delegating to an arbitrary type 代理任意的类型

下面这段有段没懂,感觉可能也用不到,就不翻译了。

It is possible that none of the options above can represent the type you want to delegate to. For example, let’s define a mapper class which is parametrized with an object and defines a map method which returns an object of another type:

class Mapper<T,U> {                             
    final T value                               
    Mapper(T value) { this.value = value }
    U map(Closure<U> producer) {                
        producer.delegate = value
        producer()
    }
}
The mapper class takes two generic type arguments: the source type and the target type
The source object is stored in a final field
The map method asks to convert the source object to a target object

As you can see, the method signature from map does not give any information about what object will be manipulated by the closure. Reading the method body, we know that it will be the value which is of type T, but T is not found in the method signature, so we are facing a case where none of the available options for @DelegatesTo is suitable. For example, if we try to statically compile this code:

def mapper = new Mapper<String,Integer>('Hello')
assert mapper.map { length() } == 5

Then the compiler will fail with:

Static type checking] - Cannot find matching method TestScript0#length()

In that case, you can use the type member of the @DelegatesTo annotation to reference T as a type token:

class Mapper<T,U> {
    final T value
    Mapper(T value) { this.value = value }
    U map(@DelegatesTo(type="T") Closure<U> producer) {  
        producer.delegate = value
        producer()
    }
}
The @DelegatesTo annotation references a generic type which is not found in the method signature

Note that you are not limited to generic type tokens. The type member can be used to represent complex types, such as List<T> or Map<T,List<U>>. The reason why you should use that in last resort is that the type is only checked when the type checker finds usage of @DelegatesTo, not when the annotated method itself is compiled. This means that type safety is only ensured at the call site. Additionally, compilation will be slower (though probably unnoticeable for most cases).

Leave a Comment

Back to Top