本文翻译自官方文档
上一篇地址
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
}
- 通过
TimeCategory
,给Integer
添加了一个minute
属性。 - 类似的,
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 的首选平台。 使用闭包,可以很容易创建自定义控制结构,也很容易创建构建器。假设您有以下代码:
代码语言:text复制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:
通常通过一下方式实现构造器:
代码语言:text复制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:
上面代码的另一个问题是它与静态类型检查不兼容,类型检查可以让问题在编译时而不是运行时就暴露出来,但是如果您尝试对此代码执行类型检查:
代码语言:text复制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:
最终它有可能会报错:
代码语言:text复制[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:
代码语言:text复制@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).
再这种模式下,我们告诉编译器我们只会再我们注解的类型里面解析(即使是注解类型的子类型,子类型里面定义的内容也是不可以见的)
代码语言:text复制但是我觉得这个地方说得不对,也可能是我没理解不对。
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:
在这里,我打算直接代理一个变量
代码语言:text复制上面的例子,都是写死了被代理的对象类型,现在希望我们提供什么类型,就代理什么类型
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:
上面的例子,只有一个闭包,但是如果有多个闭包的情况:
代码语言:text复制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:
还有更牛逼了,多个代理变量、多个闭包。
代码语言:text复制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知道我们代理的是什么类型呢:
代码语言:text复制注意这里的策略是
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:
然后这么使用:
代码语言:text复制@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:
代码语言:text复制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 |
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:
代码语言:text复制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 | |
---|---|
Note that you are not limited to generic type toke