简体   繁体   English

使用@Field注释关闭Groovy 2.4变量范围

[英]Groovy 2.4 variable scope in closure with @Field annotation

Can someone explain to me why in closure2 initVars('c') is not able to modify the referenced object if @Field is used in declaration? 有人可以向我解释为什么在initVars('c')如果在声明中使用@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
}

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. 没有@Fielddef输出,在脚本范围内只有lines4 = "a" 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. 我在groovy2.5-beta和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: 在脚本变量上使用@Field注释会将此变量的范围从本地变量更改为Script类1:

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. 用于将脚本中的变量范围从脚本的run方法更改为脚本的类级别的变量注释。

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). 在这个例子中,没有注释,变量awe将是一个本地脚本变量(从技术上讲,它将是脚本类的run方法中的局部变量)。 Such a local variable would not be visible inside the awesum method. 在awesum方法中不会看到这样的局部变量。 With the annotation, awe becomes a private List field in the script class and is visible within the awesum method. 使用注释,awe成为脚本类中的私有List字段,并在awesum方法中可见。

Source: http://docs.groovy-lang.org/2.4.12/html/gapi/groovy/transform/Field.html 资料来源: 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脚本都扩展了groovy.lang.Script类,脚本的主体在Script.run()方法中执行。 Groovy passes variables to this script using Binding object. Groovy使用Binding对象将变量传递给此脚本。 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 . 当您将本地脚本变量的范围更改为类级别时,传递给闭包的此变量没有绑定,因为binding对象仅包含本地范围的变量 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: 第一个显示第一次调用initVars(String pref)binding对象的样子,而lines4是本地脚本变量:

在此输入图像描述

And here is same breakpoint but now lines4 is a @Field def lines4 variable: 这里是相同的断点,但现在lines4@Field def lines4变量:

在此输入图像描述

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. 正如您所看到的, binding对象中的lines4变量没有绑定,但是有一个名为lines4的类字段,而此绑定存在于附加的第一个屏幕截图中。

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. 在第一个闭包中,创建了lines4局部绑定,并使用this.lines4值的当前值初始化它。 It happens because Script.getProperty(String property) is implemented in following way: 这是因为Script.getProperty(String property)以下列方式实现:

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 资料来源: 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. 因此,它首先检查是否存在对您在闭包中访问的变量的绑定以及何时不存在它将执行传递给父的getProperty(name)实现 - 在我们的示例中它只返回类属性值。 At this point this.lines4 is equal to b and this is the value that is returned. 此时this.lines4等于b ,这是返回的值。

initVars(String pref) method accesses class field, so when you call it it always overrides Script.lines4 property. initVars(String pref)方法访问类字段,因此当您调用它时,它始终会覆盖Script.lines4属性。 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. 在第二个闭包中,闭包的绑定lines4已经存在,其值为bp1 - 该值在第一个闭包调用中关联。 That's why you don't see c after calling initVars('c') . 这就是为什么你在调用initVars('c')后没有看到c的原因。 I hope it helps. 我希望它有所帮助。

UPDATE: How binding works in a script explained 更新:脚本中的binding如何解释

Let'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: 这是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();
}

Two things worth mentioning at this moment: 此时值得一提的两件事:

  1. @Field def lines4 is compiled to a class field java.lang.Object lines4; @Field def lines4被编译成类字段java.lang.Object lines4;
  2. void initVars(String pref) method is compiled to public void initVars(java.lang.String); void initVars(String pref)方法被编译为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. 为简单起见,您可以假设脚本的其余内容(不包括lines4initVars方法)内联到public java.lang.Objectrun()方法。

initVars always accesses class field lines4 because it has direct access to this field. initVars始终访问类字段lines4因为它可以直接访问此字段。 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. 操作56是用于为字段分配值的操作码。

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. 首先要提到的是 - 两个闭包都delegate字段设置为正在执行的脚本对象。 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. 我们知道它扩展了groovy.lang.Script类 - 一个使用binding私有字段来存储脚本运行时中可用的所有绑定(变量)的类。 This is important observation, because groovy.lang.Script class overrides: 这是重要的观察,因为groovy.lang.Script类重写:

Both methods use binding to lookup and store variables used in the script runtime. 两种方法都使用binding来查找和存储脚本运行时中使用的变量。 getProperty gets called any time you read local script variable and setProperty gets called any time you assign a value to script local variable. getProperty读取本地脚本变量时都会调用getProperty并且setProperty为脚本局部变量赋值时都会调用setProperty 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). 在您的示例中,第一次尝试读取lines4最终会从父类返回一个值(如果未找到绑定,则会发生这种情况,然后GroovyObjectSupport.getProperty(name)并返回具有给定名称的类属性的值)。 When closure assigns a value to a lines4 variable then a binding is created. 当closure为lines4变量赋值时,则创建绑定。 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. 并且因为两个闭包共享相同的binding对象(它们使用委托给同一个实例),当第二个闭包读取或写入line4变量时,它使用先前创建的绑定。 And initVars does not modify binding because as I shown you earlier it accesses class field directly. 并且initVars不会修改绑定,因为正如我之前所示,它直接访问类字段。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM