倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

Closure クラスの API を使ってみる (2) : this, owner, and delegate

今回はクロージャを込み入った方法で使う際に意識しておくべき3つのオブジェクト

  • this (thisObject)
  • owner
  • delegate

とそれに関するメソッドを見ていきます。 まぁ、この記事読んだところで、結局よぉ分からんってオチになりそうだけど。

this, owner, and delegate


とりあえず Groovy のドキュメント「Groovy - Closures」を参照してみると

  • this : as in Java, this refers to the instance of the enclosing class where a Closure is defined
  • owner : the enclosing object (this or a surrounding Closure)
  • delegate : by default the same as owner, but changeable for example in a builder or ExpandoMetaClass

と書かれてます。 this はクロージャの定義を含んでいるクラスのインスタンスowner はクロージャの定義を含んでいるオブジェクト(this オブジェクトまたは Closure オブジェクト)、delegate はデフォルトでは owner と同じだけど変更可能、といったところでしょうか。 これら3つのオブジェクトは全て同じオブジェクトがセットされていることも多いようです:

  • delegate を別途セットしなければ delegate と owner は同じ
  • クロージャの中でクロージャを定義すると、this と owner は異なるオブジェクトがセットされる

まぁ、サンプル・コードを見た方がはやいと思いますが、その前にこれらのオブジェクトを取得するメソッドを見ておきましょう。

各種オブジェクトを取得するメソッド

    Object getDelegate()
    void setDelegate(Object delegate)
    Object getOwner()
    Object getThisObject()

getThisObject() メソッドは Javadoc に何も書かれてませんが、「this」で参照されるオブジェクトでしょう。 delegate には getter/setter メソッドが定義されてますが、owner, this には setter メソッドがないので変更不可です*1

サンプル・コード from 『Groovy in Action』

んでは、サンプル・コード。 まずは『Groovyイン・アクション』からサンプルを拝借(ちょっと改変):

class Mother{
    int field = 1
    int foo(){ return 2 }
    Closure birth(param){
        def local = 3
        return { caller ->
            [
                this,  // Mother object
                field,  // field of Mother ( == 1 )
                foo(),  // method of Mother ( return 2 )
                local,  // local variable ( == 3 )
                param,  // parameter of birth method
                caller,  // parameter of Closure
                owner   // Mother object ( == this in this case)
             ]
        }
    }
}

Mother julia = new Mother()

c = julia.birth(4)  // param == 4
context = c(this)  // caller is instance of Script

assert context[0].class == Mother
assert context[1..4] == [1, 2, 3, 4]
assert context[5] instanceof Script
assert context[6] instanceof Mother

クロージャの定義が Mother クラスの中にあるので、this も owner も Mother オブジェクトがセットされます(julia)。 このサンプルでは delegate が使われておらず、また owner と this が同じなので、3つのオブジェクトの違いがよくわかりませんな。 次はこんなサンプル:

def c = {
    println '// in closure "c"'
    println 'delegate : ' + delegate.class
    println 'owner    : ' + owner.class
    println 'this     : ' + this.class
    println()
    
    return {
        println '// in result closure returned by "c"'
        println 'delegate : ' + delegate.class
        println 'owner    : ' + owner.class
        println 'this     : ' + this.class
    }
}

c()()

クロージャの中でクロージャを定義しています。 変数 c に代入されているクロージャ(外側のクロージャ)は、評価されるとクロージャ(内側のクロージャ)を返します。 このスクリプトを実行すると、出力結果は以下のようになります(数字部分は異なる可能性アリ):

// in closure "c"
delegate : class ConsoleScript5
owner    : class ConsoleScript5
this     : class ConsoleScript5

// in result closure returned by "c"
delegate : class ConsoleScript5$_run_closure1
owner    : class ConsoleScript5$_run_closure1
this     : class ConsoleScript5

最初の出力パートから、外側のクロージャには3つのオブジェクトとして同じもの(Script オブジェクト)がセットされていることが分かります。 一方、2つ目の出力パートから、内側のクロージャには delegate と owner として外側のクロージャが、this として Script オブジェクトがセットされてます。

関連メソッドもう少し


delegate, owner, this の3つのオブジェクトに関連する Closure クラスのメソッドは他にも幾つかあるので、それらを見ていきましょう。 Closure クラスに定義されている定数にも関係するものがあるので、そちらも載せておきます:

    Closure rehydrate(Object delegate, Object owner, Object thisObject)
    Closure dehydrate()

    int getResolveStrategy()
    void setResolveStrategy(int resolveStrategy)

    // Constants
    static int DELEGATE_FIRST
    static int DELEGATE_ONLY
    static int OWNER_FIRST    // default
    static int OWNER_ONLY
    static int TO_SELF
  • rehydrate() メソッドは delegate, owner, thisObject に引数のオブジェクトをセットしたクロージャのコピーを返します。
  • dehydrate() メソッドは3つのオブジェクトすべてに null をセットしたクロージャのコピーを返します。
  • getResolveStrategy() / setResolveStrategy() メソッドは、クロージャ内の変数の解決方法を取得・設定します。 設定可能な int 値は上記の5つ定数です。 各定数の意味はドキュメント参照。 このどれが設定されていても、ローカル変数での解決が優先されるので注意。

サンプル・コード

では、上記のメソッド(の一部)を使ったサンプル・コードを見ていきましょう。 まずは準備として、以下のような Person クラスを定義しておきます:

@groovy.transform.Canonical
class Person{
    String name
    int age
}

さて、この Person クラスのインスタンスに対して、クロージャを使ってプロパティをセットしてやります。 Groovy コードなら、Closure の API を使わずに with() メソッドを用いて

def c = {
    name = 'Waman'
    age = 100
}

def p = new Person()
p.with(c)    // クロージャでプロパティのセット

println p    // Person(Waman, 100)

とすれば OK なんですが、これを Java コードなどでもできるように Closure の API を使って行うのが目標。

delegate を設定(失敗)

まずは delegate を設定する方法。

def c = { name = 'Waman'; age = 100 }

def p = new Person()
c.delegate = p
c()

println p    // Person(null, 0)

でもこれは失敗。 うーむ、owner が Script だからまずいのかな?

rehydrate() メソッドで owner を設定

デフォルトの resolveStrategy は OWNER_FIRST なので owner に Person オブジェクトをセットしておけばうまくいくようです:

def c = { name = 'Waman'; age = 100 }

def p = new Person()
d = c.rehydrate(c.delegate, p, c.thisObject)
d()

println p    // Person(Waman, 100)

でも、rehydrate() でコピー作るのはちょっと大仰な気がしますねぇ。 どの程度コストがかかるのか知りませんが。

resolveStrategy を設定

次は delegate の設定と共に、resolveStragegy の値を DELEGATE_FIRST に設定してあげましょう:

def c = { name = 'Waman'; age = 100 }

def p = new Person()
c.resolveStrategy = Closure.DELEGATE_FIRST
c.delegate = p
c()

println p    // Person(Waman, 100)

これが一番無難そうなコードでしょうかね。 with() が使えるならそれに越したことはないですが。

プログラミングGROOVY

プログラミングGROOVY


Groovyイン・アクション

Groovyイン・アクション

*1:後で見る rehydrate() メソッドを使えば owner, thisに任意のオブジェクトを設定したクロージャを生成することができます。