今回は GDK が Object クラスに追加しているメタプログラミング用のメソッド。 Groovy ではメタオブジェクト・プロトコルという方式を使ってオブジェクトの挙動を動的に変更できるので、メタプログラミングを行いたい場合はメタクラスの設定をあれこれ行うことが多いと思います。 通常、メタクラスを設定する場合はそのメタクラスを持つオブジェクト全てについて挙動を変更したいので Class クラスからメタクラスを取得して設定を行いますが、今回扱う Object クラスに対してメタクラスの変更を行うと、その変更はそのオブジェクトだけにしか適用されないので注意。 あと、スコープを設定してオブジェクトの挙動を変更するカテゴリについても見ていきます。 今回扱うメソッドは以下のもの:
// メタクラス MetaClass getMetaClass() MetaClass metaClass(Closure closure) void setMetaClass(MetaClass metaClass) // メタプロパティ MetaProperty hasProperty(String name) List getMetaPropertyValues() // (メタ)メソッド List respondsTo(String name, Object[] argTypes) List respondsTo(String name) // カテゴリ Object use(Class categoryClass, Closure closure) Object use(List categoryClassList, Closure closure) Object use(Object[] array)
もう少し各メソッドを見てみるとこんな感じ:
メソッド | 返り値 | since | 説明 |
---|---|---|---|
getMetaClass() setMetaClass(MetaClass) metaClass(Closure) |
MetaClass void MetaClass |
1.5.0 1.6.0 1.6.0 |
メタクラスを取得 メタクラスをセット クロージャと ExpandoMetaClass によるメタプログラミング |
hasProperty(String) getMetaPropertyValues() |
MetaProperty List |
1.6.1 1.0 |
指定した名前のメタプロパティを持っているか? メタプロパティの値のリストを取得 |
respondsTo(String, Object[]) respondsTo(String) |
List*1 | 1.6.0 1.6.1 |
指定した名前と引数の(メタ)メソッドがあるか? |
use(Class, Closure) use(List, Closure) use(Object[]) |
Object | 1.0 | カテゴリを使う |
ではそれぞれのメソッドの使い方を見ていきましょう。 以下では次のように定義された Person クラスとそのインスタンス waman があるとします:
class Person{ String name int age } def waman = new Person(name:'倭マン', age:100)
メタクラス関連
メタクラス関連のメソッドは
- getMetaClass()
- setMetaClass(MetaClass)
- metaClass(Closure)
getMetaClass(), setMetaClass() メソッドは特に問題ないと思います。 オブジェクトに対して行う(Class リテラルに対してではなく)と各インスタンス毎の設定になるところだけ注意が必要ですが。 メタクラスを使ってメタプログラミング(Java ではできないクラスの挙動の変更など)するには metaClass() メソッドを使います。
// metaClass() によるメタプログラミング waman.metaClass{ getSex = { -> 'male' } isMale = { -> sex == 'male' } //introduce{ "私の名前は${delegate.name}です。 年齢は${delegate.age}歳です。" } introduce{ "My name is $delegate.name, $delegate.age years old." } greet << { Person someone -> "こんにちは、${someone.name}さん。" } greet << { String name, int age -> greeting(new Person(name:name, age:age)) } } assert waman.sex == 'male' assert waman.male assert waman.introduce() == 'My name is 倭マン, 100 years old.'
上記の例では getSet(), isMale(), introduce(), greet() メソッド(×2)を定義しています。 getSex(), isMale() などはプロパティとして「waman.sex」、「waman.male」のようにアクセスすることができます。
メソッドを定義する場合は
- 《メソッド名》{ メソッド本体 }
- 《メソッド名》 = { メソッド本体 }
- 《メソッド名》 << { メソッド本体 }
の3バージョンがありますが、「=」と「<<」の違いは同名同シグニチャのメソッドがあった場合に「=」は置き換え、「<<」は例外スローだそうです(『プログラミングGROOVY』より)。 ただ、Groovy 2.1.4 で試してみたところ、全て置き換えでした???
ちなみに、何度か言っているようにオブジェクトに対してメタクラスの変更をしても他のインスタンスには(同クラスでも)影響ありません:
def wasan = new Person(name:'倭算', age:10000) try{ wasan.introduce() assert false }catch(MissingMethodException ex){ assert true }
同クラスのインスタンスに対してメタプログラミングを行いたい場合はクラス・リテラルからメタクラスを取得して設定する必要があります:
Person.metaClass{
...
}
ちなみに、これは(Person)インスタンス生成の前に行う必要があります。 このあたりの話は GDK の Class 編をもしやることがあれば・・・
メタプロパティ、メタメソッド
次はメタプロパティ関連のメソッド。 メタプロパティとはなんぞや?と聞かれてもお答えしかねるので(単に getter and/or setter があるプロパティみたいな感じ?)、サンプルコードの挙動で推察しておくんなまし。 以下のコードは、上記のメタクラス変更を行った後に実行してます:
// hasProperty(), getMetaPropertyValues() assert waman.hasProperty('class') assert waman.hasProperty('name') assert waman.hasProperty('age') assert waman.hasProperty('sex') // メタクラスに追加した getSex() メソッド assert waman.hasProperty('male') // メタクラスに追加した isMale() メソッド assert waman.getMetaPropertyValues().collect{ it.value }.toSet() == [Person, '倭マン', 100, 'male', true].toSet() // メタプロパティの値を取得。 順序はコードの通りとは限らない
次はメタメソッド。 こちらも意味はよく分からん。 responseTo() メソッドは Class クラスの getMethods() メソッドみたいなもので、指定した名前のメソッドを取得するメソッド。 パラメータの型を指定するものもあります(getMethods() にもあるように):
// respondsTo() assert waman.respondsTo('getSex').size() == 1 assert waman.respondsTo('introduce').size() == 2 // 引数なしと引数1つ assert waman.respondsTo('greet').size() == 2 assert waman.respondsTo('greet', Person).size() == 1 assert waman.respondsTo('greet', String, int).size() == 1 assert waman.respondsTo('greet', int, String).isEmpty()
メタクラスでクロージャによってメソッドを追加する際、クロージャに引数を指定していなかったら引数なしと引数1つのものが追加されます。 まぁ、あんまり問題はなさそうかな。
カテゴリ
最後は use() メソッドを使ったカテゴリの使用。 (Person クラスに対する)カテゴリは例えば以下のような第1引数が Person の static メソッド introdceAffectedly() (カッコつけた自己紹介)を定義したクラスのことです
class PersonCategory1{ static introduceAffectedly(Person person){ "『${person.introduce()}』" } }
このクラスと use() メソッドを使って、あたかも Person オブジェクトに introduceAffectedly() メソッドがあるかのようにコードを書くことができます:
use(PersonCategory1){ assert waman.introduceAffectedly() == '『My name is 倭マン, 100 years old.』' }
static メソッドで書くのが好きじゃないという方には、@Category アノテーションを使って
@Category(Person) class PersonCategory2{ def introduceAAffectedly(){ "『『${this.introduce()}』』" } } use(PersonCategory2){ assert waman.introduceAAffectedly() == '『『My name is 倭マン, 100 years old.』』' }
のように書くこともできます。 複数のカテゴリを使用したい場合は
// use(Object[]) 配列の最後の要素がクロージャ use(PersonCategory1, PersonCategory2){ assert waman.introduceAffectedly() == '『My name is 倭マン, 100 years old.』' assert waman.introduceAAffectedly() == '『『My name is 倭マン, 100 years old.』』' }
のようにできます。 ちなみに、カテゴリは特にそれ用のインターフェースなどがあるわけではなく、static メソッドの第1引数が使用したいオブジェクトの型ならどんなクラスでも使用できます。 特に Java ライブラリでユーティリティ・クラスとして作成されたクラスはカテゴリとして使用できるものもよくあります。 例えば java.nio.file.Files クラスは同パッケージ内の Path オブジェクトに対するユーティリティ・メソッドがたくさん定義されてますが、第1引数が Path のメソッドばかりなので、カテゴリとして使用することができます:
import java.nio.file.* import java.nio.charset.Charset def log = Paths.get('log.txt') use(Files){ // Files をカテゴリとして使用 if(log.exists())log.delete() log.createFile() log.write(['I am waman?'], Charset.defaultCharset()) } def linesep = System.getProperty('line.separator') assert log.toFile().text == "I am waman?$linesep"
他にも標準 API やサードパーティ製のユーティリティ・クラスでもこういった使い方ができるクラスは多いかと。
今回で GDK が Object クラスに追加しているメソッドは終了。 次回はやっと Collection かな?(予定)
- 作者: 関谷和愛,上原潤二,須江信洋,中野靖治
- 出版社/メーカー: 技術評論社
- 発売日: 2011/07/06
- メディア: 単行本(ソフトカバー)
- 購入: 6人 クリック: 392回
- この商品を含むブログ (152件) を見る
*1:List<? extends MetaMethod>