Can someone explain to me why in closure2 initVars('c')
is not able to modify the referenced object if @Field
is used in declaration?
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
}
Output:
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
Output without @Field
and def
, with just lines4 = "a"
in script scope. This appears normal to me.
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
I saw same behavior in groovy2.5-beta and groovy 2.6-alpha.
Using @Field
annotation on a script variable changes a scope of this variable from a local one to a Script
class one:
Variable annotation used for changing the scope of a variable within a script from being within the run method of the script to being at the class level for the script.
The annotated variable will become a private field of the script class. The type of the field will be the same as the type of the variable. Example usage:
import groovy.transform.Field @Field List awe = [1, 2, 3] def awesum() { awe.sum() } assert awesum() == 6
In this example, without the annotation, variable awe would be a local script variable (technically speaking it will be a local variable within the run method of the script class). Such a local variable would not be visible inside the awesum method. With the annotation, awe becomes a private List field in the script class and is visible within the awesum method.
Source: http://docs.groovy-lang.org/2.4.12/html/gapi/groovy/transform/Field.html
Every Groovy script extends groovy.lang.Script
class and the body of the script is executed inside Script.run()
method. Groovy passes variables to this script using Binding
object. When you change a scope of a local script variable to a class level then there is no binding for this variable passed to a closure, because binding
object contains only local-scoped variables. Compare these two screenshots I made. First one shows what the binding
object looks like when we call initVars(String pref)
for the first time and lines4
is a local script variable:
And here is same breakpoint but now lines4
is a @Field def lines4
variable:
As you can see there is no binding for lines4
variable in binding
object, but there is a class field called lines4
, while this binding is present in the first screenshot attached.
When you call
lines4 += 'p1'
in the first closure, local binding for lines4
is created and it is initialized with a current value of a this.lines4
value. It happens because Script.getProperty(String property)
is implemented in following way:
public Object getProperty(String property) {
try {
return binding.getVariable(property);
} catch (MissingPropertyException e) {
return super.getProperty(property);
}
}
Source: https://github.com/apache/groovy/blob/GROOVY_2_4_X/src/main/groovy/lang/Script.java#L54
So it firstly checks if there is a binding for a variable you access in the closure and when it does not exist it passes execution to a parent's getProperty(name)
implementation - in our case it just returns class property value. At this point this.lines4
is equal to b
and this is the value that is returned.
initVars(String pref)
method accesses class field, so when you call it it always overrides Script.lines4
property. But when you call
lines4 += 'q1'
in the second closure, the binding lines4
for a closure already exists and its value is bp1
- this value was associated in the first closure call. That's why you don't see c
after calling initVars('c')
. I hope it helps.
binding
works in a script explainedLet's get a little deeper to get better understanding what is going on under the hood. This is what your Groovy script looks like when it is compiled to a bytecode:
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();
}
Two things worth mentioning at this moment:
@Field def lines4
is compiled to a class field java.lang.Object lines4;
void initVars(String pref)
method is compiled to public void initVars(java.lang.String);
class method.For a simplicity you can assume that the rest content (excluding lines4
and initVars
method) of your script is inlined to public java.lang.Objectrun()
method.
initVars
always accesses class field lines4
because it has direct access to this field. Decompiling this method to a bytecode shows us this:
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
Operation 56 is a opcode for a assigning value to a field.
Now let's understand what happens when both closures gets called. First thing worth mentioning - both closures have delegate
field set to the script object that is being executed. We know that it extends groovy.lang.Script
class - a class that uses binding
private field to store all bindings (variables) available in the script runtime. This is important observation, because groovy.lang.Script
class overrides:
public Object getProperty(String property)
public void setProperty(String property, Object newValue)
Both methods use binding
to lookup and store variables used in the script runtime. getProperty
gets called any time you read local script variable and setProperty
gets called any time you assign a value to script local variable. That's why code like:
lines4 += 'p1'
generates sequence like:
getProperty -> value + 'p1' -> setProperty
In your example first attempt of reading lines4
ends up with returning a value from parent class (it happens if binding is not found, then GroovyObjectSupport.getProperty(name)
is called and this one returns a value of a class property with given name). When closure assigns a value to a lines4
variable then a binding is created. And because both closures share same binding
object (they use delegate to the same instance), when second closure reads or writes line4
variable then it uses previously created binding. And initVars
does not modify binding because as I shown you earlier it accesses class field directly.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With