ラッパークラスの生成

メソッドの呼び出しをログに書き出すようにしておくと、トラブル時に役に立ちます。でも、そのようなトレース書き出しをあちこちに埋め込むと、ソースコードが読みにくくなってしまいます。.NETではRealProxyを使ってメソッドの呼び出しに割り込めて、所謂、アスペクト指向プログラミングっぽいことが出来るので素晴らしいのですが、パフォーマンスがとても悪かったりします。やりたいことは、メソッドの呼び出しに割り込めれば良いだけなので、もう少し単純な方法を考えてみましょう。例えば次のようなクラスがあったとします。

public class Calc
{
    public virtual int Add(int a, int b)
    {
        return a + b;
    }

    public virtual int Subtract(int a, int b)
    {
        return a - b;
    }
}

このクラスのAddとSubtractに割り込んで、メソッドの呼び出しをコンソールに書き出したいとします。

public class WrappedCalc : Calc
{
    public override int Add(int a, int b)
    {
        Console.WriteLine("Calc#Add");
        return base.Add(a, b);
    }

    public override int Subtract(int a, int b)
    {
        Console.WriteLine("Calc#Subtract");
        return base.Subtract(a, b);
    }
}

・・・呆れられそう。(^^;

ですが、これが自動生成可能でしたら、ちょっとは反応が違ってくると思います。そこでTypeBuilderとGenericsを使ってラッパー生成クラスを作ってみました。

#region Using directives

using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.Reflection.Emit;

#endregion

/// <summary>
/// Tの仮想関数をラップし、メソッド名を表示させるクラス
/// </summary>
class TypeWrapper
{
    private static Dictionary<string, Type> dict=new Dictionary<string,Type>();

    public static T Wrap<T>() where T : class, new()
    {
        Type t = typeof(T);
        Type wt;

        // クラス名にWrappedを付ける
        string name = "Wrapped" + t.Name;
        // 既に作成済みのラップクラスか?
        if (dict.TryGetValue(name, out wt))
            return Activator.CreateInstance(wt) as T;

        AssemblyBuilder ab = AppDomain.CurrentDomain.DefineDynamicAssembly(
            new AssemblyName("Wrapped" + t.Name + "Assembly"), AssemblyBuilderAccess.Run);
        ModuleBuilder md = ab.DefineDynamicModule(
            "Wrapped" + t.Name + "Module");
        TypeBuilder tb = md.DefineType("Wrapped" + t.Name, TypeAttributes.Class, t);
        tb.DefineDefaultConstructor(MethodAttributes.Public);

        foreach (MethodInfo mi in t.GetMethods(
            BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance))
        {
            // 仮想関数のみオーバーライドする
            if (mi.IsVirtual)
            {
                // パラメータの型
                ParameterInfo pis = mi.GetParameters();
                Type types = new Type[pis.Length];
                for (int i = 0; i < pis.Length; ++i)
                    types[i] = pis[i].ParameterType;
            
                // メソッドのシグネチャはオーバーライド対象と同じ    
                MethodBuilder mb = tb.DefineMethod(mi.Name, mi.Attributes, 
                    mi.CallingConvention, mi.ReturnParameter.ParameterType, types);

                ILGenerator il = mb.GetILGenerator();
                // 割り込んでメソッド名を表示
                il.EmitWriteLine(t.Name + "#" + mi.Name);
                // パラメータをスタックに積む
                // thisパラメータもスタックに積むので+1している
                for (int i = 0; i < pis.Length + 1; ++i)
                    il.Emit(OpCodes.Ldarg, i);
                // ベースクラスのメソッド呼び出し
                il.Emit(OpCodes.Call, mi);
                il.Emit(OpCodes.Ret);

                // オーバーライド
                tb.DefineMethodOverride(mb, mi);
            }
        }
        wt = tb.CreateType();
        dict[name] = wt;
        return Activator.CreateInstance(wt) as T;
    }
}

// テスト用クラス
public class Calc
{
    public virtual int Add(int a, int b)
    {
        return a + b;
    }

    public virtual int Subtract(int a, int b)
    {
        return a - b;
    }
}

class Program
{
    static void Main(string[] args)
    {
        // Calcのラッパーを生成
        Calc calc = TypeWrapper.Wrap<Calc>();
        // メソッドを呼び出すとメソッド名が表示される
        Console.WriteLine(calc.Add(10, 20));
        Console.WriteLine(calc.Subtract(50, 30));
        Console.ReadLine();
    }
}

/* 結果
Calc#Add
30
Calc#Subtract
20
 */

割り込ませているコードがイマイチなので有り難みが無いかも知れませんが、もう少し気の利いた処理を入れられるようにすれば、結構使えそうな気が・・・しませんか?(^^;