有人可以向我解释为什么在声明中使用initVars('c')
时,{2} @Field
无法修改引用的对象?
import groovy.transform.Field;
@Field def lines4 = "a";
void initVars(String pref){
println('init:'+lines4+' '+pref) //*3.init:a b *7.init:b c
lines4 = pref;
}
println("closure1") ///1. closure1
1.times {
println(lines4) ///2. a
initVars('b') ///3. init:a b
lines4 += 'p1'
println(lines4) ///4. bp1
}
println("closure2") ///5. closure2
1.times {
println(lines4) ///6. bp1
initVars('c') ///7. init:b c
println(lines4) ///8. bp1 Why not c
lines4 += 'q1'
println(lines4) ///9. bp1q1 Why not cq1
}
输出:
C:\projects\ATT>groovy test.groovy
1. closure1
2. a
3. init:a b
4. bp1
5. closure2
6. bp1
7. init:b c
8. bp1
9. bp1q1
没有@Field
和def
的输出,脚本范围内只有lines4 = "a"
。这看起来很正常。
C:\projects\ATT>groovy test.groovy
1. closure1
2. a
3. init:a
4. bp1
5. closure2
6. bp1
7. init:bp1
8. c
9. cq1
我在groovy2.5-beta和groovy 2.6-alpha中看到了相同的行为。
答案 0 :(得分:5)
在脚本变量上使用@Field
注释会将此变量的范围从本地变量更改为Script
第1类:
用于将脚本中的变量范围从脚本的run方法更改为脚本的类级别的变量注释。
带注释的变量将成为脚本类的私有字段。字段的类型将与变量的类型相同。用法示例:
import groovy.transform.Field @Field List awe = [1, 2, 3] def awesum() { awe.sum() } assert awesum() == 6
在这个例子中,没有注释,变量awe将是一个本地脚本变量(从技术上讲,它将是脚本类的run方法中的局部变量)。在awesum方法中不会看到这样的局部变量。使用注释,awe成为脚本类中的私有List字段,并在awesum方法中可见。
来源:http://docs.groovy-lang.org/2.4.12/html/gapi/groovy/transform/Field.html
每个Groovy脚本都扩展groovy.lang.Script
类,脚本的主体在Script.run()
方法中执行。 Groovy使用Binding
对象将变量传递给此脚本。当您将本地脚本变量的范围更改为类级别时,传递给闭包的此变量没有绑定,因为binding
对象仅包含本地范围的变量。比较我制作的这两个截图。第一个显示binding
对象在我们第一次调用initVars(String pref)
时的样子,lines4
是本地脚本变量:
这是相同的断点,但现在lines4
是@Field def lines4
变量:
正如您所看到的,lines4
对象中的binding
变量没有绑定,但是有一个名为lines4
的类字段,而此绑定存在于附加的第一个屏幕截图中。
致电时
lines4 += 'p1'
在第一个闭包中,创建lines4
的局部绑定,并使用当前值this.lines4
初始化它。这是因为Script.getProperty(String property)
以下列方式实现:
public Object getProperty(String property) {
try {
return binding.getVariable(property);
} catch (MissingPropertyException e) {
return super.getProperty(property);
}
}
来源:https://github.com/apache/groovy/blob/GROOVY_2_4_X/src/main/groovy/lang/Script.java#L54
因此,它首先检查是否存在对您在闭包中访问的变量的绑定以及何时不存在它将执行传递给父的getProperty(name)
实现 - 在我们的示例中它只返回类适当的价值。此时this.lines4
等于b
,这是返回的值。
initVars(String pref)
方法访问类字段,因此当您调用它时,它始终会覆盖Script.lines4
属性。但是当你打电话时
lines4 += 'q1'
在第二个闭包中,闭包的绑定lines4
已经存在,其值为bp1
- 该值在第一个闭包调用中关联。这就是为什么在致电c
后你没有看到initVars('c')
的原因。我希望它有所帮助。
binding
如何在脚本中解释让我们更深入一点,以便更好地了解幕后发生的事情。这是Groovy脚本在编译为字节码时的样子:
Compiled from "script_with_closures.groovy"
public class script_with_closures extends groovy.lang.Script {
java.lang.Object lines4;
public static transient boolean __$stMC;
public script_with_closures();
public script_with_closures(groovy.lang.Binding);
public static void main(java.lang.String...);
public java.lang.Object run();
public void initVars(java.lang.String);
protected groovy.lang.MetaClass $getStaticMetaClass();
}
此时值得一提的两件事:
@Field def lines4
已编译为类字段java.lang.Object lines4;
void initVars(String pref)
方法编译为public void initVars(java.lang.String);
类方法。为简单起见,您可以假设脚本的其他内容(不包括lines4
和initVars
方法)内联到public java.lang.Objectrun()
方法。
initVars
始终访问类字段lines4
,因为它可以直接访问此字段。将此方法反编译为字节码向我们显示:
public void initVars(java.lang.String);
Code:
0: invokestatic #19 // Method $getCallSiteArray:()[Lorg/codehaus/groovy/runtime/callsite/CallSite;
3: astore_2
4: aload_2
5: ldc #77 // int 5
7: aaload
8: aload_0
9: aload_2
10: ldc #78 // int 6
12: aaload
13: aload_2
14: ldc #79 // int 7
16: aaload
17: aload_2
18: ldc #80 // int 8
20: aaload
21: ldc #82 // String init:
23: aload_0
24: getfield #23 // Field lines4:Ljava/lang/Object;
27: invokeinterface #67, 3 // InterfaceMethod org/codehaus/groovy/runtime/callsite/CallSite.call:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
32: ldc #84 // String
34: invokeinterface #67, 3 // InterfaceMethod org/codehaus/groovy/runtime/callsite/CallSite.call:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
39: aload_1
40: invokeinterface #67, 3 // InterfaceMethod org/codehaus/groovy/runtime/callsite/CallSite.call:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
45: invokeinterface #52, 3 // InterfaceMethod org/codehaus/groovy/runtime/callsite/CallSite.callCurrent:(Lgroovy/lang/GroovyObject;Ljava/lang/Object;)Ljava/lang/Object;
50: pop
51: aload_1
52: astore_3
53: aload_3
54: aload_0
55: swap
56: putfield #23 // Field lines4:Ljava/lang/Object;
59: aload_3
60: pop
61: return
操作56是用于为字段分配值的操作码。
现在让我们了解两个闭包被调用时会发生什么。首先要提到的是 - 两个闭包都将delegate
字段设置为正在执行的脚本对象。我们知道它扩展了groovy.lang.Script
类 - 一个使用binding
private field来存储脚本运行时中可用的所有绑定(变量)的类。这是重要的观察,因为groovy.lang.Script
类覆盖:
public Object getProperty(String property)
public void setProperty(String property, Object newValue)
两种方法都使用binding
来查找和存储脚本运行时中使用的变量。每次读取本地脚本变量时都会调用getProperty
,并且在为脚本局部变量赋值时随时调用setProperty
。这就是为什么代码如下:
lines4 += 'p1'
生成如下序列:
getProperty -> value + 'p1' -> setProperty
在您的示例中,首次尝试读取lines4
最终会从父类返回一个值(如果未找到绑定,则会发生这种情况,然后GroovyObjectSupport.getProperty(name)
is called,并且此返回类属性的值给定的名称)。当闭包为lines4
变量赋值时,则创建绑定。并且因为两个闭包共享相同的binding
对象(它们使用委托给同一个实例),当第二个闭包读取或写入line4
变量时,它使用先前创建的绑定。并且initVars
不会修改绑定,因为正如我之前所示,它直接访问类字段。