groovy - 带有@Field批注的Groovy 2.4变量范围

标签 groovy

有人可以向我解释为什么在声明中使用initVars('c')时,在closure2中@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中看到了相同的行为。

最佳答案

在脚本变量上使用@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对象仅包含本地范围的变量。比较我制作的这两个屏幕截图。第一个显示了当我们第一次调用bindinginitVars(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();
}

目前有两件事值得一提:
  • @Field def lines4编译为一个类字段java.lang.Object lines4;
  • void initVars(String pref)方法被编译为public void initVars(java.lang.String);类方法。

  • 为简单起见,您可以假定脚本的其余内容(不包括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类覆盖:
  • 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不会修改绑定,因为正如我之前所展示的,它直接访问类字段。

    关于groovy - 带有@Field批注的Groovy 2.4变量范围,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46579944/

    相关文章:

    用于检查网站是否启动的 Jenkins Pipeline 脚本

    java - groovy/java 正则表达式检查是否为 "yyyy.MM.dd"格式

    java.lang.NoClassDefFoundError : groovy/lang/GroovyObject 错误

    java - 如何为 grails 编写 XLSX 自定义渲染器

    groovy - 如何在 Geb 测试中导航回浏览器历史记录

    grails - 如何在Grails中的gsp上根据环境在Config.groovy中设置值并获得相同参数的不同值?

    json - 想要 Groovy MarkupBuilder() 等同于对象的 JSONBuilder()

    Groovy - 设置您自己的整数属性

    java - 时间执行 Groovy 与 Beanshell 的巨大差异

    groovy - 如何消除编译任务期间的随机常规错误