CLOVER🍀

That was when it all began.

Groovyのバインディング変数とローカル変数

Groovyでは、スクリプト中の変数のスコープが宣言方法で大きく変わるようです。これも仕事でGroovyを使っていて、ハマったのでメモ。

例えば、以下のスクリプトについて。
binding_local.groovy

outside = "binding object"
def outsideLocal = "local object"

println(outside) // OK
println(outsideLocal) // OK

def func() {
    println(outside) // OK
    //println(outsideLocal) // groovy.lang.MissingPropertyException: No such property
}

func()

この例では、2つの変数outsideとoutsideLocalをそれぞれスクリプトトップレベル中の処理とメソッド内で参照しようとしていますが、変数outsideLocalをメソッド内で参照している方はコメントにもある通り例外を送出して処理に失敗します。

実際、実行するとこんな感じです。

$ groovy binding_local.groovy 
binding object
local object
binding object
Caught: groovy.lang.MissingPropertyException: No such property: outsideLocal for class: binding_local
groovy.lang.MissingPropertyException: No such property: outsideLocal for class: binding_local
	at binding_local.func(binding_local.groovy:9)
	at binding_local.run(binding_local.groovy:12)

2つの変数の違いは、宣言時にdefキーワードの有無しか違いがないのに…。

他の言語ではこういうことは普通にできていた気がするので、最初意味がわかりませんでした。宣言を明示したいなぁくらいの感覚でdefを使っていたので、ものは試しとdefを外してみたら動作するではありませんか。

これはいったい、どういうことでしょう?

ちょっと気になったので、このスクリプトをgroovycしてできたクラスファイルを、JDで逆コンパイルしてみました。結果は、こちら。

import groovy.lang.Binding;
import groovy.lang.Script;
import org.codehaus.groovy.runtime.BytecodeInterface8;
import org.codehaus.groovy.runtime.ScriptBytecodeAdapter;
import org.codehaus.groovy.runtime.callsite.CallSite;
import org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation;

public class binding_local extends Script
{
  public binding_local()
  {
    binding_local this;
    CallSite[] arrayOfCallSite = $getCallSiteArray();
  }

  public binding_local(Binding arg1)
  {
    Binding context;
    CallSite[] arrayOfCallSite = $getCallSiteArray();
    ScriptBytecodeAdapter.invokeMethodOnSuperN($get$$class$groovy$lang$Script(), this, "setBinding", new Object[] { context });
  }

  public static void main(String[] args)
  {
    CallSite[] arrayOfCallSite = $getCallSiteArray();
    arrayOfCallSite[0].call($get$$class$org$codehaus$groovy$runtime$InvokerHelper(), $get$$class$binding_local(), args);
  }

  public Object run()
  {
    CallSite[] arrayOfCallSite = $getCallSiteArray(); String str = "binding object"; ScriptBytecodeAdapter.setGroovyObjectProperty(str, $get$$class$binding_local(), this, "outside");
    Object outsideLocal = "local object";

    arrayOfCallSite[1].callCurrent(this, arrayOfCallSite[2].callGroovyObjectGetProperty(this));
    arrayOfCallSite[3].callCurrent(this, outsideLocal); if ((__$stMC) || (BytecodeInterface8.disabledStandardMetaClass()))
    {
      return arrayOfCallSite[4].callCurrent(this); } else return func(); return null;
  }

  public Object func()
  {
    CallSite[] arrayOfCallSite = $getCallSiteArray(); return arrayOfCallSite[5].callCurrent(this, arrayOfCallSite[6].callGroovyObjectGetProperty(this)); return null;
  }

  static
  {
    __$swapInit();
    Long localLong1 = (Long)DefaultTypeTransformation.box(0L);
    __timeStamp__239_neverHappen1332081958027 = localLong1.longValue();
    Long localLong2 = (Long)DefaultTypeTransformation.box(1332081958026L);
    __timeStamp = localLong2.longValue();
  }
}

注目すべきは

  public Object run()
  {
    CallSite[] arrayOfCallSite = $getCallSiteArray(); String str = "binding object"; ScriptBytecodeAdapter.setGroovyObjectProperty(str, $get$$class$binding_local(), this, "outside");
    Object outsideLocal = "local object";

    arrayOfCallSite[1].callCurrent(this, arrayOfCallSite[2].callGroovyObjectGetProperty(this));
    arrayOfCallSite[3].callCurrent(this, outsideLocal); if ((__$stMC) || (BytecodeInterface8.disabledStandardMetaClass()))
    {
      return arrayOfCallSite[4].callCurrent(this); } else return func(); return null;
  }

ここでしょうか。明らかに、変数outsideを特別扱いしていますね。outsideLocalの方は、普通にローカル変数として定義されています。

それにしても、スクリプト中のトップレベルの変数は普通にスクリプトから生成されるクラスのフィールドになると思っていたんですけど、違うんですね。

こちらの書籍にも書いていました。

プログラミングGROOVY

プログラミングGROOVY

今回の例だと、

と呼ぶらしいです。また、変数outsideLocalも@Fieldアノテーションを付与することにより、フィールドに昇格することができるそうな。

outside = "binding object"
@groovy.transform.Field
def outsideLocal = "local object"

ちなみに、クロージャからはバインディング変数もローカル変数も、普通に参照可能です。

def clos = {
    println(outside)
    println(outsideLocal)
}

それにしても、なんでこんな仕様になってるんでしょ?