uehaj's blog

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

ExpandoMetaClass.enableGlobally()について一言いっておくか

前から書こうと思ってたのですが、つい遅くなりました。
以下でid:genzouwさんや、id:odashinsukeさんが報告されている:

問題についてですが、結論から言うと

ExpandoMetaClass.enableGlobally()

を呼ぶことで解決されます。
このメソッドは、ExpandoMetaClassによるメソッド拡張を、メソッド追加前にnewされたインスタンスにさかのぼって適用するものです。継承されたサブクラスに対しても有効にする、という作用もあるようです。

「ExpandMetaClassによるメソッド拡張」というのは、具体的には、metaClassプロパティにクロージャを使ってメソッドを代入するという行為を指します。これはプロパティに対する代入というだけではなく、初回の代入時には、クラスのメタクラスをExpandoMetaClassに差し替える、という副作用を導きます。MetaClass#setProperty()がそういう風に作られているのでしょう。

このことを確かめるには、例えば

class C{}
println C.metaClass
C.metaClass.a = {->}
println C.metaClass

とかやってみますと、
metaClassのプロパティへの代入の前と後で、

org.codehaus.groovy.runtime.HandleMetaClass@19b46dc[groovy.lang.MetaClassImpl@19b46dc[class C]]
groovy.lang.ExpandoMetaClass@1922f46[class C]

のようにメタクラスがExpandoMetaClassに差し変わっていることが分かります。

話を戻すと、ExpandoMetaClass.enableGlobally()を呼ばない限り、インスタンスを生成してしまった後のメソッド追加は有効になりません。すでに生成してしまったインスタンスのメタクラスについてもExpandoMetaClassが差し換わっている状態にするためにはExpandoMetaClass.enableGlobally()が呼ばれている必要があります。

//ExpandoMetaClass.enableGlobally()
class C{}
C c = new C();
println c.metaClass
C.metaClass.a = {->println "hello"}
println c.metaClass
C d = new C();
println d.metaClass

こういうコードを、enableGlobally()をコメントアウトして実行すると

org.codehaus.groovy.runtime.HandleMetaClass@136a1a1[groovy.lang.MetaClassImpl@136a1a1[class C]]
org.codehaus.groovy.runtime.HandleMetaClass@136a1a1[groovy.lang.MetaClassImpl@136a1a1[class C]]
org.codehaus.groovy.runtime.HandleMetaClass@db23f1[groovy.lang.ExpandoMetaClass@db23f1[class C]]

であり、有効にして実行すると

org.codehaus.groovy.runtime.HandleMetaClass@195dd5b[groovy.lang.ExpandoMetaClass@195dd5b[class C]]
org.codehaus.groovy.runtime.HandleMetaClass@195dd5b[groovy.lang.ExpandoMetaClass@195dd5b[class C]]
org.codehaus.groovy.runtime.HandleMetaClass@195dd5b[groovy.lang.ExpandoMetaClass@195dd5b[class C]]

と表示されます。つまり、enableGlobally()は実際には全てのクラスのメタクラスをあらかじめExpandoMetaClassにしてしまう働きがあるということですね

推測ですが、EMCは、自分のご存じないメソッド呼び出しを、元のメタクラスにディスパッチする働きを持ってます。つまり上位互換なわけです。みんなのメタクラスをEMCにしてしまえば、問題ない。

上のようなことはGroovyドキュメントにかかれてませんね、ということが以下でjiraにあがってます。

その答えはというと、「Unsetting the fix version. Can be done anytime for any version. 」
がくぅという感想を持たざるを得ない。登録日付は「25/Oct/07 08:04 AM」なので忘れてるとしか思えんな・・・。

さて、以上が前置きです:)

問題をややこしくする事情がもうひとつあって、それは、「enableGloballyしないとメソッド追加が有効にならない」という振る舞いは、java.lang.StringとかのPOJOでは起きないということです。つまり、StringとかIntegerに関しては、ExpandoMetaClass.enableGlobally()を呼ばなくても、インスタンスが生成された後に追加されたEMCのメソッドが有効になるのです。

このことは、上記のようなことも分かってなくて、最初バグだと思ってjiraに報告したときに教えてもらった解説によって理解しました(本当に理解しているかどうかは自信が無いですが)。上によると、GroovyObjectのサブクラスは(つまりGroovyで定義したクラスや、groovy.lang.Closureとか)は、もともとメタクラスを持っていて、それに対してEMCが使われたときには、生成済みのインスタンスは差し替え前のメタクラスを保持し続けるのでEMCが参照されないのです。しかし、Javaのクラスはもともとメタクラスを持たないので、差し替えの必要も無く、旧インスタンスでもEMCが使えるらしい。(ここらへんは正直よく分かってない推測です。「差し替え」というイメージじゃない気がしてきた。あらかじめ使われているかどうか、か。)

仕組みはともかく、この振る舞いが、多くの人が、ExpandoMetaClass.enableGloball()の呼び出しの必要性を誤解する1つの理由だと思います*1。つまり、JavaのクラスのStringとかでは動的に反映されるのに、Groovyのクラスだと(enableGlobally呼ばない限り)動的ではないのです。普通逆じゃろと。

POJOと同様の感覚で、enableGlobally呼ばないで、例えば、クロージャにEMC経由でメソッドを追加しようとすると、はまるわけであり、id:uokumraさんも「Closureにメソッド足すにはExpandoMetaClass.enableGlobally()が必要」の記事で解析されています。

以上が私の理解です。

まとめると、

  • Groovyで定義したクラスや、groovy.lang.*等で定義されている、groovy.lang.Closure,SciptなどのGroovyObjectのサブクラスについては、生成済みのインスタンスにもEMC追加メソッドの効果を及ぼしたいなら、ExpandoMetaClass.enableGloball()を呼ぶべし!

あ、あとインスタンスに対するメタクラスのケースに関しては、例外の種別がなんとなく変な(少なくとも不親切な)気がしますね。よくわからないですが。

以上でした。

*1:ドキュメントが修正されてないのも致命的だと思うけど。