倭マン's BLOG

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

あんたに GDK の何が分かるっていうの!? (Object 編6) メタプログラミング用メソッド

今回は 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 かな?(予定)

プログラミングGROOVY

プログラミングGROOVY

*1:List<? extends MetaMethod>