uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかReactとかFregeとかJavaとか -

クロージャにとってdelegateとは何か

短い答え

短い答え: クロージャにとって、delegateってのは、変更可能なthisですよ。

長い答え

メソッドにとってthisとは何だったか

まず、thisとは何だったかを確認します。thisの中核的な意味は、インスタンスメソッドから見て自分自身が所属するインスタンスを指すということですね。それはOK。
thisの振舞いには、も1つ着目すべきことあります。

class C {
  void method() {}
  int field;
  void foo() {
    method(); // this.method()
    System.out.println(field); // this.field
  }
}

上のメソッドfoo()の定義で、this.method(),this.fieldと書くかわりに、thisを省略してmethod(),fieldと書くことができています。これが意味するのは、コンパイラがmethod,fieldという識別子が何かを判断する処理において、Cクラスのメンバーからも探索してくれるということです。

thisが省略されたとき、thisが指しているインスタンスは、識別子を探すための名前空間として使われます*1

クロージャに話を戻す

さて、クロージャです。クロージャは関数とくらべると、名前がない、ということに特徴があります。
さらに、クロージャメソッドとくらべると、無名であるだけでなく、「クラスに所属しない」という特徴もあります。

クロージャメソッドのように使うとしたら?

そんなクロージャメソッドのようにつかいたい、とします。
具体的には、EMC(Expando Meta Class)の場合。

String.metaClass.hello = { println "hello "+it }
"ABC".hello("people") //==> "hello people"が表示される

これは良いでしょう。
さて、hello()をよびだすhelloWorld()を定義したいとします。

String.metaClass.hello = { println "hello "+ it }
String.metaClass.helloWorld = { hello("world") }
"ABC".helloWorld() //==> "hello world"が表示される

あっけなくできます。
でも、これが可能なのは自明ではありません。なぜhelloWorld()からhello()が「見える」のでしょうか。クロージャをmetaClassのプロパティに代入する前は、見えなかったはずです。

分解してみます。

String.metaClass.hello = { println "hello "+ it }
Closure c= { hello("world") }
c.call() // ==> No signature of method例外が発生する。
// cからhello()は見えない。この時点でcはStringやhelloと無関係だから当然。

String.metaClass.helloWorld = c
"ABC".helloWorld() //==> (A)"hello world"が表示される

ふむ。何か秘密があるようです。
いきなり種あかしをすると、(A)の時点でクロージャdelegateプロパティが設定されるからです。delegateプロパティの値と型を表示させてみます。

String.metaClass.hello = { println "hello "+ it }
Closure c= { println "[3]${delegate},${delegate.class}"; hello("world") }
println "[1]${c.delegate},${c.delegate.class}"
String.metaClass.helloWorld = c
println "[2]${c.delegate},${c.delegate.class}"
"ABC".helloWorld() //==> "hello world"が表示される
println "[4]${c.delegate},${c.delegate.class}"

以下が表示されます。

[1]test@d6089a5,class test
[2]test@d6089a5,class test
[3]ABC,class java.lang.String
hello world
[4]test@d6089a5,class test

[3]で、delegateプロパティにStringインスタンス"ABC"が設定されていることがわかります。クロージャ呼び出しの直前に設定されて、終了後[4]には戻されていることもわかります。ちなみに、このスクリプトはtest.groovyというスクリプトなので、[1][2][4]ではそれが表示されています*2

ExpandoMetaClassが、delegateに、「インスタンスに相当するオブジェクト」を適切に「thisのようなもの」として設定してくれるので、クロージャインスタンスを介してメソッドを参照しあえるのです。フィールドの参照についても同様です。幸せなことです。

delegateを設定してみる

metaClass(ExpandoMetaClass)にやれるなら、自分でやれないはずはない。自前でやってみます。

Closure c= { hello("world") }

class MyClass {
  def hello(s){println "hello MyClass "+s}
}

c.delegate = new MyClass()
c.call()
// "hello MyClass world" が表示される。

できた。

まとめ

delegateは、thisの1つの働きとしてそうであるように、クロージャにおいて、識別子を探すための名前空間として動作します。加えて、delegateクロージャのプロパティなので任意に変更可能です。これらの特徴によって、delegateはうまく設定することで、クロージャをあたかもメソッドのように動作させることに役立ちます。ただし用途はそれに限るわけではなく、Builderとかでも多用されています。

delegateは、上の理屈からすると、設定するとき以外はつかわなくても良い(省略して暗に使われる)ものである気もしますが、実際には明に使うことも結構良くあります。少なくともJavaでthisキーワードを使う程度には使います。理解しておくのが吉でしょう。

おしまい。

*1:この言い方は不正確かもしれません。Groovyでは動的に、Javaでは一部静的に解決されますので、いっしょくたに言うための言い方です。

*2:delegateの初期値はそのクロージャが位置しているメソッドが所属するクラスのインスタンスということです。