読者です 読者をやめる 読者になる 読者になる

プログラミング C# - 翔ソフトウェア (Sho's)

C#/.NET/ソフトウェア開発など

メタプログラミング入門 - 応用編 - オブジェクトの文字列変換のメタプログラミング (Reflection.Emit 編)

.NET C#
Metasequoia

※ 「[C#][.NET] メタプログラミング入門 - 応用編 - オブジェクトの文字列変換のメタプログラミング」の続き。

Reflection.Emit によるメタプログラミング

それでは、Reflection.Emit を用いて文字列生成を行う例を見ていこう。

題材は 「[C#][.NET] メタプログラミング入門 - 応用編 - オブジェクトの文字列変換を静的/動的に行う」の中の「(デバッグ用の) 文字列に変換」だ。

今回も同様に、例えば、次のようなクラスのオブジェクトを文字列に変換する。

// テスト用のクラス
public sealed class Book
{
    public string Title { get; set; }
    public int    Price { get; set; }
}
Reflection.Emit によって生成したいプログラムの例

この Book クラスの場合、プログラムによって生成したいプログラムは、例えば、次のようなものだ。

    // 動的に作りたいコードの例:
    public static string ToString(Book book)
    {
        StringBuilder stringBuilder;
        stringBuilder       = new StringBuilder();
        stringBuilder.Append("Title: ");
        var           title = book.Title;
        stringBuilder.Append(title);
        stringBuilder.Append(", ");
        stringBuilder.Append("Price: ");
        var           price = book.Price;
        stringBuilder.Append(price);
        return stringBuilder.ToString();
    }
ILSpy を使って IL を見る

今回も、「[C#][.NET] メタプログラミング入門 - Reflection.Emit による Add メソッドの動的生成」でやったように、ILSpy を使ってこのコードの IL (Intermediate Language) を表示し、参考にしよう。

上記 ToString(Book book) メソッドを含むプログラムをビルドし (Release ビルド)、ILSpy で開くと次のように表示される。

ILSpy で Call メソッドの IL を見る
ILSpy で ToString メソッドの IL を見る

これを参考に IL を生成するコードを書いていこう。

これから書くプログラムは生成する方で、このプログラムで生成されるプログラムと混同しないように注意する必要がある。例えば、このプログラム生成プログラムは、Book クラスに依存しないように書く必要がある。動的で汎用的なものだ。これに対して、生成されるコードの方は、対象とするオブジェクトのクラス (例えば Book クラス) 専用の静的で高速なものだ。

表にしてみよう。

プログラム 手書き/生成 汎用性 プログラムの動作 動作速度
文字列変換プログラムを生成するプログラム 手書き 対象とするオブジェクトのクラス (例えば Book クラス) に依存せず汎用的 動的 遅い
文字列変換プログラム プログラムによって生成 対象とするオブジェクトのクラス (例えば Book クラス) 専用 静的 速い
Reflection.Emit によるオブジェクトの文字列への変換プログラム生成プログラム

では、IL を参考にプログラム生成プログラムを作ろう。

using System;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;

// ToString メソッド生成器 (Reflection.Emit 版)
public static class ToStringGeneratorByEmit
{
    // メソッドを生成
    public static Func<T, string> Generate<T>()
    {
        // DynamicMethod
        var method = new DynamicMethod(
            name          : "ToString"         ,
            returnType    : typeof(string)     ,
            parameterTypes: new { typeof(T) }
        );

        // 引数 item 生成用
        var item      = method.DefineParameter(position: 1, attributes: ParameterAttributes.In, parameterName: "item");
        // ILGenerator
        var generator = method.GetILGenerator();
        Generate(generator, typeof(T));

        // 動的にデリゲートを生成
        return (Func<T, string>)method.CreateDelegate(delegateType: typeof(Func<T, string>));
    }

    // メソッドを生成
    static void Generate(ILGenerator ilGenerator, Type targetType)
    {
        // 対象とする型の全プロパティ情報を取得
        var properties = targetType.GetProperties(bindingAttr: BindingFlags.Public | BindingFlags.Instance).Where(property => property.CanRead).ToArray();
        if (properties.Length > 0) {
            // 動的に作りたいコードの例 (実際のコードは targetType による):
            //public static string ToText(Book book)
            //{
            //    StringBuilder stringBuilder;
            //    stringBuilder = new StringBuilder();
            //    stringBuilder.Append("Title: ");
            //    var title     = book.Title;
            //    stringBuilder.Append(title);
            //    stringBuilder.Append(", ");
            //    stringBuilder.Append("Price: ");
            //    var price     = book.Price;
            //    stringBuilder.Append(price);
            //    return stringBuilder.ToString();
            //}

            // 動的に作りたい IL:
            // newobj instance void [mscorlib]System.Text.StringBuilder::.ctor()
            // stloc.0
            // ldloc.0

            var stringBuilderType = typeof(StringBuilder);
            // 「ローカルに StringBuilder を宣言する」コードを追加
            ilGenerator.DeclareLocal(localType: stringBuilderType);
            // 「StringBuilder のコンストラクターを使ってインスタンスを new する」コードを追加
            ilGenerator.Emit(opcode: OpCodes.Newobj, con: stringBuilderType.GetConstructor(Type.EmptyTypes));
            // 「現在の値 (StringBuilder のインスタンスへの参照) をスタックからポップし、ローカル変数に格納する」コードを追加
            ilGenerator.Emit(opcode: OpCodes.Stloc_0);
            // 「ローカル変数 (StringBuilder のインスタンスへの参照) をスタックに読み込む」コードを追加
            ilGenerator.Emit(opcode: OpCodes.Ldloc_0);

            // プロパティ毎に文字列に変換するコードを生成
            for (var index = 0; index < properties.Length; index++)
                GenerateForEachProperty(ilGenerator: ilGenerator, property: properties[index], needsSeparator: index < properties.Length - 1);

            // 「スタックからポップする」コードを追加
            ilGenerator.Emit(opcode: OpCodes.Pop);
            // 「ローカル変数 (StringBuilder のインスタンスへの参照) をスタックに読み込む」コードを追加
            ilGenerator.Emit(opcode: OpCodes.Ldloc_0);
            // 「StringBuilder のバーチャル メソッド ToString を呼ぶ」コードを追加
            ilGenerator.Emit(opcode: OpCodes.Callvirt, meth: stringBuilderType.GetMethod(name: "ToString", types: Type.EmptyTypes));
        } else {
            // 動的に作りたい IL:
            // ldstr ""

            // 「空の文字列をプッシュする」コードを追加
            ilGenerator.Emit(opcode: OpCodes.Ldstr, str: string.Empty);
        }
        // 動的に作りたい IL:
        // ret

        // 「リターンする」コードを追加
        ilGenerator.Emit(opcode: OpCodes.Ret);
    }

    // 文字列が引数の StringBuilder.Append メソッドが何度も使われるため static メンバーに
    static readonly MethodInfo appendMethod = typeof(StringBuilder).GetMethod(name: "Append", types: new { typeof(string) });

    // プロパティ毎に文字列に変換するコードを生成
    static void GenerateForEachProperty(ILGenerator ilGenerator, PropertyInfo property, bool needsSeparator)
    {
        // 動的に作りたいコードの例 (実際のコードは property による):
        // stringBuilder.Append("Title: ");
        // var title = item.Title;
        // stringBuilder.Append(title);

        // 動的に作りたい IL の例:
        // ldstr "Title: "
        // callvirt instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string)
        // ldarg.0
        // callvirt instance string Book::get_Title()
        // callvirt instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string)

        // 「プロパティ名 + ": " をプッシュする」コードを追加
        ilGenerator.Emit(opcode: OpCodes.Ldstr, str: property.Name + ": ");
        // 「文字列が引数の StringBuilder.Append を呼ぶ」コードを追加
        ilGenerator.Emit(opcode: OpCodes.Callvirt, meth: appendMethod);

        // 「インスタンスへの参照をプッシュする」コードを追加
        ilGenerator.Emit(opcode: OpCodes.Ldarg_0);
        var propertyGetMethod = property.GetGetMethod(); // 渡されたプロパティの get メソッド
        // 「渡されたプロパティの get メソッドを呼ぶ」コードを追加
        ilGenerator.Emit(propertyGetMethod.IsVirtual ? OpCodes.Callvirt : OpCodes.Call, propertyGetMethod);

        var propertyGetMethodReturnType = propertyGetMethod.ReturnType; // 渡されたプロパティの get メソッドの戻り値の型
        //  渡されたプロパティの get メソッドの戻り値の型が引数の StringBuilder.Append メソッド
        var typedAppendMethod = typeof(StringBuilder).GetMethod(name: "Append", types: new[] { propertyGetMethodReturnType });

        // 型が違っていて、値型だった場合はボクシングするコードを追加
        if (typedAppendMethod.GetParameters()[0].ParameterType != propertyGetMethodReturnType &&
            propertyGetMethodReturnType.IsValueType)
            ilGenerator.Emit(opcode: OpCodes.Box, cls: propertyGetMethodReturnType);

        // 「渡されたプロパティの get メソッドの戻り値の型が引数の StringBuilder.Append メソッドを呼ぶ」コードを追加
        ilGenerator.Emit(opcode: OpCodes.Callvirt, meth: typedAppendMethod);

        if (needsSeparator) {
            // 動的に作りたいコード:
            // stringBuilder.Append(", ");

            // 動的に作りたい IL:
            // ldstr ", "
            // callvirt instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string)

            // 「", " をプッシュする」コードを追加
            ilGenerator.Emit(opcode: OpCodes.Ldstr, str: ", ");
            // 「文字列が引数の StringBuilder.Append メソッドを呼ぶ」コードを追加
            ilGenerator.Emit(opcode: OpCodes.Callvirt, meth: appendMethod);
        }
    }
}
Reflection.Emit によるオブジェクトの文字列への変換 (キャッシュ無し)

それでは、これを使って変換メソッドを作ろう。先ずは、単純な「メソッドをキャッシュをせず、毎回コード生成するもの」から。

// 改良前 (メソッドのキャッシュ無し)
public static class ToStringByEmitExtensions初期型
{
    // ToString に代わる拡張メソッド (Reflection.Emit 版)
    public static string ToStringByEmit初期型<T>(this T @this)
    {
        // 動的にメソッドを生成し、それを実行
        return ToStringGeneratorByEmit.Generate<T>()(@this);
    }
}
メソッドのキャッシュ無し版の動作テスト

次のような簡単なプログラムで動作させてみよう。

using System;

static class Program
{
    static void Main()
    {
        var book = new Book { Title = "Metaprogramming C#", Price = 3200 };

        Console.WriteLine(book.ToStringByEmit初期型());
    }
}

実行結果は次のようになる。

Title: Metaprogramming C#, Price: 3200

正しく動作する。

生成したメソッドのキャッシュ

上の "ToStringByEmit初期型" メソッドでは、呼び出される度にメソッドを生成している。 これでは、その度に時間が掛かる。 一度生成したメソッドは、キャッシュしておくことにしたい。

型毎に1つずつメソッドが必要なので、型毎に最初にメソッドが必要になったときにだけ生成し、それをキャッシュしておくことにしよう。 2回目からは、キャッシュにあるメソッドを使用するのだ。

この為、先にメソッド キャッシュ クラスを作成しよう。 次のようなものだ。Dictionary の中に型情報をキーとしてメソッドを格納する。 このクラスでは、ジェネリックを使い型に依存しないようにする。

using System;
using System.Collections.Generic;

//  生成したメソッド用のキャッシュ
public class MethodCache<TResult>
{
    // メソッド格納用
    readonly Dictionary<Type, Delegate> methods = new Dictionary<Type, Delegate>();

    // メソッドの呼び出し (メソッド生成用のメソッドを引数 generator として受け取る)
    public TResult Call<T>(T item, Func<Func<T, TResult>> generator)
    {
        return Get<T>(generator)(item); // キャッシュにあるメソッドを呼び出す
    }

    // メソッドをキャッシュを介して取得 (メソッド生成用のメソッドを引数 generator として受け取る)
    Func<T, TResult> Get<T>(Func<Func<T, TResult>> generator)
    {
        var      targetType = typeof(T);
        Delegate method;
        if (!methods.TryGetValue(key: targetType, value: out method)) { // キャッシュに無い場合は
            method = generator();                                       // 動的にメソッドを生成して
            methods.Add(key: targetType, value: method);                // キャッシュに格納
        }
        return (Func<T, TResult>)method;
    }
}
Reflection.Emit によるオブジェクトの文字列への変換 (キャッシュ有り)

では、キャッシュを行う「オブジェクトの文字列への変換」を作成しよう。

上のメソッド キャッシュ クラス MethodCache を利用して、次のようにする。

// 改良後 (メソッドのキャッシュ有り)
public static class ToStringByEmitExtensions改
{
    // 生成したメソッドのキャッシュ
    static readonly MethodCache<string> toStringCache = new MethodCache<string>();

    // ToString に代わる拡張メソッド (Reflection.Emit 版)
    public static string ToStringByEmit改<T>(this T @this)
    {
        // キャッシュを利用してメソッドを呼ぶ
        return toStringCache.Call(item: @this, generator: ToStringGeneratorByEmit.Generate<T>);
    }
}
メソッドのキャッシュ有り版の動作テスト

こちらも動作させてみよう。

using System;

static class Program
{
    static void Main()
    {
        var book = new Book { Title = "Metaprogramming C#", Price = 3200 };

        Console.WriteLine(book.ToStringByEmit改());
    }
}

勿論、実行結果は同じだ。

Title: Metaprogramming C#, Price: 3200

まとめ

今回は、Reflection.Emit を用いて、動的に「オブジェクトを文字列に変換する」メソッドを生成するプログラムを作成した。

次回は、式木を使って同様のことを行う。