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

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

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

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

.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; }
}
式木によって生成したいプログラムの例

この Book クラスの場合、プログラムによって生成したい「文字列変換を行うラムダ式」は、例えば、次のようなものだ。

    // 動的に作りたいラムダ式の例 (実際のコードは targetType による):
    item => new StringBuilder().Append("Title: ").Append(item.Title)
                               .Append(", ")
                               .Append("Price: ").Append(item.Price)
                               .ToString()

このようなラムダ式を、これまで何度か行ったように、動的に組み立てることにしよう。

但し、上のラムダ式は、Book クラスの場合の例で、対象とするオブジェクトのクラスによって、必要なラムダ式は異なる。

そのため、ラムダ式を生成する部分は、リフレクションを用いて動的に行うことにする。 また、ジェネリックを用いて、型に依存しないようにする。

生成したラムダ式は、コンパイルしてデリゲートにする。 デリゲートになってしまえば、それは静的なコードと同様に動作させることができる。

そして、今回もデリゲートをキャッシュしておくことで、文字列変換を行う度に毎回デリゲートを動的に生成しなくても良いようにしよう。

式木によるオブジェクトの文字列への変換プログラム生成プログラム

では、上のようなラムダ式を生成し、コンパイルしてデリゲートとするプログラムを作ろう。

using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;

// ToString メソッド生成器 (式木版)
public static class ToStringGeneratorByExpression
{
    // メソッドを生成
    public static Func<T, string> Generate<T>()
    {
        // 対象とする型の全プロパティ情報を取得
        var            properties                  = typeof(T).GetProperties(bindingAttr: BindingFlags.Public | BindingFlags.Instance)
                                                              .Where(property => property.CanRead)
                                                              .ToArray();
        var parameterExpression = Expression.Parameter(typeof(T), "item");
        LambdaExpression lambdaExpression;
        if (properties.Length > 0) {
            // 動的に作りたいラムダ式の例 (実際のコードは targetType による):
            // item => new StringBuilder().Append("Title: ").Append(item.Title)
            //                            .Append(", ")
            //                            .Append("Price: ").Append(item.Price)
            //                            .ToString()

            var        callGetTypeOfItemExpression = Expression.Call(
                                                         instance: parameterExpression           ,
                                                         method  : typeof(T).GetMethod("GetType")
                                                     );
            Expression stringBuilderExpression     = Expression.New(type: typeof(StringBuilder));

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

            stringBuilderExpression                = stringBuilderExpression.CallMethod(typeof(StringBuilder).GetMethod("ToString", Type.EmptyTypes));
            lambdaExpression                       = Expression.Lambda(stringBuilderExpression, parameterExpression);
        } else {
            // 動的に作りたいラムダ式の例:
            // item => string.Empty
            lambdaExpression = Expression.Lambda(Expression.Constant(string.Empty, typeof(string)),
                                                 parameterExpression);
        }
        // ラムダ式をコンパイルしてデリゲートとして返す
        return (Func<T, string>)lambdaExpression.Compile();
    }

    // 何度も使われるメソッドは static メンバーに
    static readonly MethodInfo stringBuilderAppendStringMethod = typeof(StringBuilder).GetMethod("Append"     , new { typeof(string) });
    static readonly MethodInfo stringBuilderAppendObjectMethod = typeof(StringBuilder).GetMethod("Append"     , new { typeof(object) });
    static readonly MethodInfo typeGetPropertyMethod           = typeof(Type         ).GetMethod("GetProperty", new { typeof(string) });
    static readonly MethodInfo propertyInfoGetValueMethod      = typeof(PropertyInfo ).GetMethod("GetValue"   , new { typeof(object) });

    // プロパティ毎に文字列に変換する式を生成
    static Expression GenerateForEachProperty(PropertyInfo property, Expression expression, ParameterExpression parameterExpression, MethodCallExpression callGetTypeOfItemExpression, bool needsSeparator)
    {
        // 例えば、item.Title の式を生成 (実際のコードは property による)
        var callGetValueExpression = callGetTypeOfItemExpression
                                     .CallMethod(typeGetPropertyMethod         , Expression.Constant(property.Name)        )
                                     .CallMethod(propertyInfoGetValueMethod    , parameterExpression                       );

        // 例えば、stringBuilder.Append("Title: ").Append(item.Title) の式を生成 (実際のコードは property による)
        expression                 = expression
                                     .CallMethod(stringBuilderAppendStringMethod, Expression.Constant(property.Name + ": "))
                                     .CallMethod(stringBuilderAppendObjectMethod, callGetValueExpression                   );

        // 必要なら、stringBuilder.Append(", ") の式を生成
        if (needsSeparator)
            expression = expression.CallMethod(stringBuilderAppendStringMethod, Expression.Constant(", "));

        return expression;
    }

    // Expression.Call をメソッドチェーンにするための拡張メソッド
    static Expression CallMethod(this Expression @this, MethodInfo method, params Expression[] arguments)
    {
        return Expression.Call(@this, method, arguments);
    }
}
式木によるオブジェクトの文字列への変換 (キャッシュ無し)

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

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

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

using System;

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

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

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

Title: Metaprogramming C#, Price: 3200

正しく動作する。

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

前回同様、キャッシュを利用してみよう。

前回作成したメソッド キャッシュ クラスは次のようなものだった。

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;
    }
}
式木によるオブジェクトの文字列への変換 (キャッシュ有り)

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

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

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

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

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

using System;

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

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

実行結果は同じだ。

Title: Metaprogramming C#, Price: 3200

まとめ

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

次回は、Roslyn による方法だ。