Groovyのクロージャ ②クロージャのthisとownerとdelegate
背景
JenkinsというCIツールではGroovyのコードでCI挙動を記述できるJenkins pipelineがあります。 Jenkins pipelineではGroovyのDSL(Domain-Specific Language:ドメイン固有言語)が使われています。 DSLにはクロージャが効果的に使用されています。
今回はクロージャのthisとownerとdelegateです。
前回の記事
前回の記事で、 クロージャとは生まれ故郷(クロージャ自身が定義された場所)を忘れない無名関数のこと、 クロージャは、クロージャが定義された場所で動く(ように見える)、ということが確認できました。
クロージャが故郷を忘れないためにしていること
クロージャが実行されたときどのようなことが起こっているのでしょうか。どのようにして元のクラスのメソッドを呼ぶのでしょうか。
その答えは、クロージャが故郷(クロージャが定義された場所)の情報を持っているからです。
クロージャはオブジェクトなので、内部に情報を持てます。クロージャのインスタンス変数にはthis
、owner
、delegate
の3つが定義されています。
それぞれ以下の情報が入っています。
フィールド | 入ってる情報 |
---|---|
this | クロージャを囲んでいるクラス |
owner | クロージャを囲んでいるオブジェクト。クラスか、クロージャを囲んでいるクロージャ |
delegate | 外部のオブジェクト。メソッドやプロパティ名が元のオブジェクトで解決できないときに呼ばれる |
確認してみましょう。
class Main { // 普通のインスタンスメソッド def closureTest() { Closure cl = { a() } println "this : ${cl.thisObject}" println "owner : ${cl.owner}" println "delegate : ${cl.delegate}" } def a() { println "I'm Main.a" } } def main = new Main() main.closureTest()
実行結果です。
this : Main@2e654c59 owner : Main@2e654c59 delegate : Main@2e654c59
すべてMainクラスのインスタンスという結果でした。クロージャの中にさらにクロージャがある場合(クロージャがネストしている場合)にはownerとdelegateの値が、対象のクロージャを囲むクロージャになります。詳しくはこちらを参照してください。
ここまでで、クロージャは故郷を忘れないためにthis
とowner
に生まれた場所の情報を格納していることが分かりました。
delegate
は、デフォルトではowner
と同じですが、クロージャが名前解決に使用するオブジェクトを格納するために使用されます。
delegateにクラスを指定する
delegate
にクラスを指定します(正確にいうと、クラスのインスタンスを指定します)。Pipelineクラスのpipeline()
メソッド内で、クロージャのdelegate
を変更します。
PipelineSpecクラスにa()
メソッドを定義しました。Mainクラスにはa()
メソッドはありません。
class PipelineSpec { def a() { println "I'm PipelineSpec.a" } } class Pipeline { def pipeline(Closure cl) { cl.delegate = new PipelineSpec() println "before closure" cl() println "after closure" } } class Main { def closureTest() { def pipeline = new Pipeline() pipeline.pipeline { a() } } } def main = new Main() main.closureTest()
実行結果です。
before closure I'm PipelineSpec.a after closure
クロージャcl
内のa()
メソッドは、PipelineSpecのa()
メソッドを呼んでいます。
このように、owner
のオブジェクトに該当するメソッドがない場合は、delegate
に指定されたオブジェクトのメソッドを実行します。