モチベーション
- なんとなく、呼び出した回数をプログラム自身が確認したくなったり。
- クラスのメソッド全てに一律にデバッグ用の機能を入れたい。
等の例として。
解決方法
例えば、次のクラス定義を考える。
# pythonじゃないよ!Booだよ!
class A:
def foo(): # ... foo()メソッド本体 ...
def bar(): # ... bar()メソッド本体 ...
このクラスのメソッド foo() と bar() の呼び出し回数を記録する機能を手作業で追加するなら次のようになる。
# pythonじゃないよ!Booだよ!
class A:
count_foo as int
count_bar as int
def foo(): ++count_foo; # ... foo()メソッド本体 ...
def bar(): ++count_bar; # ... bar()メソッド本体 ...
C# 等であればカスタム属性等でも実装することはできるが、Booらしく構文木書き換えで攻めるのが王道(?)ということで、上記の手作業をマクロで自動化する。
ソース
# python じゃないよ!Booだよ!
import Boo.Lang.Compiler.Ast
# 'invocation_counter' とクラス定義の中に書くと、
# 全メソッドに対応する '_count_メソッド名' という整数フィールドを追加し、
# 各メソッド本体の先頭でフィールドをインクリメントするコードを追加する。
macro invocation_counter:
# c = このマクロを書いた一番内側のクラス定義
c = invocation_counter.GetAncestor[of ClassDefinition]()
assert c != null
# 全メソッド定義構文木について処理をする。
for m in array(c.Members):
method = m as Method
continue if method is null or method.IsStatic # static メソッドは対応しない。
# メソッド本体先頭にはカウンタインクリメントを追加
counterName = '_count_' + method.Name
method.Body.Insert( 0, [|
++$(ReferenceExpression(counterName))
|] )
# クラスにカウンタフィールドを追加
field = Field( )
field.Name = counterName
field.Visibility = TypeMemberModifiers.Public
field.Type = SimpleTypeReference( 'int' )
c.Members.Add( field )
この invocation_counter マクロは次のように使う。
class A:
invocation_counter
def foo():
pass
def bar():
pass
def zoo():
pass
a = A()
a.foo(); a.foo(); a.foo() # 3回
a.bar() # 1回
print "foo:" + a._count_foo + " bar:" + a._count_bar + " zoo:" + a._count_zoo
# foo:3 bar:1 zoo:0 と表示される。
注意点
- staticメソッドに対応してません。対応修正は簡単です。
- 'count_メソッド名' という名前がプログラム中で定義されていると名前衝突でコンパイルエラーになります。
結論
関連プロパティを追加する例に似ているが、属性ではなくマクロでやってみた。マクロなので属性を使うためのコンパイルは不要でメンテナンスが楽だと思う。
もっと複雑な次のようなケースでも今回のメソッド内部の構文木書き換えは使えると思う。個人的には、昨今の言語に、C/C++ 言語のプリプロセッサのようなプログラムを好き勝手書き換え生成し放題の世界が還ってきた!とか思っていたりする。
本来はメソッドにわけて書かなければならない関連する処理を、1つのメソッドとして書きたい場合。
メソッドの本体を適当なルールで分割して、個別にメソッド構文木として吐き出す。分割前のメソッドは抹殺するなり、分割後のメソッドを呼び出すなど。
ステートマシンなプログラムで、各状態の初期化、本体処理、脱出処理、とか。
定義済みのメソッドを置き換えたい場合。
Unity の MonoBehaviour なシングルトンを実装するなら、
- クラスにシングルトンコードを入れる(インスタンス用の変数とか生成メソッドとか)
- 既存のAwake()やStart()は名前変更+新しいAwake()でシングルトンコード処理と改名したメソッド呼び出しを入れる。
とか。Awake()やStart()はマクロで書き換えれば、実コードの方では使っちゃダメ、とか、このメソッドをオーバーライドしなきゃダメ、みたいな制限が取っ払える。上手に作れば、プレハブsingletonとか後続のシーンにも同じものがある、とかいう場合も上手く処理できる。
おまけ
今回のサンプルみたいなことを本当にしたいなら、むしろ S2Container.netみたいな AOP をやればいいと思う。