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

时间:2017-10-05 07:15:44

标签: groovy

有人可以向我解释为什么在声明中使用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

没有@Fielddef的输出,脚本范围内只有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中看到了相同的行为。

1 个答案:

答案 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是本地脚本变量:

enter image description here

这是相同的断点,但现在lines4@Field def lines4变量:

enter image description here

正如您所看到的,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();
}

此时值得一提的两件事:

  1. @Field def lines4已编译为类字段java.lang.Object lines4;
  2. void initVars(String pref)方法编译为public void initVars(java.lang.String);类方法。
  3. 为简单起见,您可以假设脚本的其他内容(不包括lines4initVars方法)内联到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类覆盖:

    两种方法都使用binding来查找和存储脚本运行时中使用的变量。每次读取本地脚本变量时都会调用getProperty,并且在为脚本局部变量赋值时随时调用setProperty。这就是为什么代码如下:

    lines4 += 'p1'
    

    生成如下序列:

    getProperty -> value + 'p1' -> setProperty
    

    在您的示例中,首次尝试读取lines4最终会从父类返回一个值(如果未找到绑定,则会发生这种情况,然后GroovyObjectSupport.getProperty(name) is called,并且此返回类属性的值给定的名称)。当闭包为lines4变量赋值时,则创建绑定。并且因为两个闭包共享相同的binding对象(它们使用委托给同一个实例),当第二个闭包读取或写入line4变量时,它使用先前创建的绑定。并且initVars不会修改绑定,因为正如我之前所示,它直接访问类字段。