プログラミング日記というブログで、「クロージャのレキシカルスコープ」という記事で指摘されていたのですが、Expandoクラスにおいて、メソッドから参照するプロパティと、Expando内のクロージャから見たプロパティの値が異なります。
具体的には、クロージャから参照すると、値が取れるのに、メソッドからは値が取れない、という現象があります。
class Foo extends Expando { // def bar def foo() { println bar } } def f = new Foo() f.bar = 1 // プロパティbarの動的定義かつ代入 f.foo() 1が表示
まずこれはOKであり、期待する動作です。Expandoの面目躍如であり、代入するだけで簡単にプロパティbarが定義できます。
なのに、以下のように静的なプロパティとしてbarを定義すると、
class Foo extends Expando { def bar //プロパティbarの静的定義 def foo() { println bar } } def f = new Foo() f.bar = 1 // プロパティbarへの代入 f.foo() nullが表示
になってしまいます。同じレベルのプロパティであるにもかかわらず、クラスで宣言した場合と、expandoで動的に代入することによって定義した場合で参照できる値が違います。ちなみに以下のようにクロージャからならOK。
class Foo extends Expando { def bar //プロパティbarの静的定義 def foo() { def c = { println bar } c.call() } } def f = new Foo() f.bar = 1 // プロパティbarへの代入 f.foo() 1が表示
ちなみに上は、プロパティbarに整数値を入れてますが、barにクロージャを入れて呼び出そうとする場合でも同様(その場合barがnullなのでNPEになる)。
この理由はおそらく、Expandoの実装がdelegateに基づいてthisへのメソッド呼び出しをフックすることで実現しているもので、メソッドにおけるプロパティ値の取得機構には影響を与えないものだからです。
で、まあこれは仕様かバグか分かりませんが、混乱を招きますねえしょうがないですねえと思いました。関連があるかわかりませんが、ExpandoはもともとはGroovyの一部ではなく、 Grailsプロジェクトで実装されたユーザーライブラリと聞いています。Groovyのインタプリタ実装コア(メソッド実行機構・スコープ解決機構)にしかるべくあらかじめ設計されて組み込まれていたものではなかったということです。この件に関して期待する動作をするためにはおそらく、インタプリタ実装コアに手を加え、メソッドコンパイル時に生成するバイトコードを変える必要があるわけですが、そこまではExpandoは出来ないのかあるいは何らかの理由でしてないのか。
それはいいのですが、思ったのは、私としては長年の疑問であった「GrailsのControllerはメソッドではなく、アクションをクロージャを代入したプロパティで扱わなければならないのはなぜか?」について、この件が理由なのじゃないか、ということです。おそらく、Expandoと同じ制約と原理に基づく機構(Expando Meta Class)でコントロールしているのじゃないでしょうか。動的注入されたメソッドやプロパティと同様にクラスで宣言したメソッドやプロパティを参照したいなら、そのコードはメソッドではなくクロージャである必要があるということになります。
いつか確認してみたいです。